mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
Compare commits
3 Commits
master
...
1723-audio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dc12fd257 | ||
|
|
93f7122f57 | ||
|
|
af2b80a066 |
15
build.gradle
15
build.gradle
@@ -23,8 +23,8 @@ import java.text.SimpleDateFormat
|
||||
* Instructions for building/compiling the sdrtrunk application.
|
||||
*
|
||||
* Prerequisites:
|
||||
* Install and configure an OpenJDK version 19+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 7.6+
|
||||
* Install and configure an OpenJDK version 20 that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 8.2+
|
||||
*
|
||||
* Scenario 1: run the application via gradle command line from the source code root directory:
|
||||
* command: ./gradlew run
|
||||
@@ -46,6 +46,8 @@ plugins {
|
||||
id 'java'
|
||||
id 'idea'
|
||||
id 'org.beryx.runtime' version '1.12.7'
|
||||
id 'org.springframework.boot' version '3.1.5'
|
||||
id 'io.spring.dependency-management' version '1.1.3'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -92,6 +94,7 @@ dependencies {
|
||||
implementation 'ch.qos.logback:logback-core:1.4.5'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
|
||||
implementation 'com.fazecast:jSerialComm:2.9.3'
|
||||
implementation 'com.github.jiconfont:jiconfont-elusive:2.0.3'
|
||||
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
||||
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
@@ -103,6 +106,7 @@ dependencies {
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'eu.hansolo:charts:1.0.5'
|
||||
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
|
||||
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
|
||||
implementation 'javax.usb:usb-api:1.0.2'
|
||||
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
|
||||
@@ -110,15 +114,18 @@ dependencies {
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'org.apache.commons:commons-csv:1.9.0'
|
||||
implementation 'org.apache.derby:derby'
|
||||
implementation 'org.apache.derby:derbytools'
|
||||
implementation 'org.apache.mina:mina-core:2.2.1'
|
||||
implementation 'org.apache.mina:mina-http:2.2.1'
|
||||
implementation 'org.controlsfx:controlsfx:11.1.2'
|
||||
implementation 'org.rauschig:jarchivelib:1.2.0'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.5'
|
||||
implementation 'org.springframework.boot:spring-boot-starter'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java-javax:1.3.0'
|
||||
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
|
||||
implementation 'pl.edu.icm:JLargeArrays:1.6'
|
||||
}
|
||||
|
||||
@@ -230,7 +237,7 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
||||
//jdk.accessibility is used with assistive technologies like screen readers
|
||||
//java.management for JVM resource monitoring
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management', 'java.instrument')
|
||||
|
||||
//Use auto-detected modules and 'add' any specified modules.
|
||||
rt.extension.additive.set(true)
|
||||
|
||||
@@ -33,12 +33,14 @@ import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Alias Model contains all aliases and is responsible for creation and management of alias lists. Alias lists are a
|
||||
* set of aliases that all share a common alias list name and can be attached to a decoding channel for aliasing
|
||||
* identifiers produced by channel decoder(s).
|
||||
*/
|
||||
@Component("aliasModel")
|
||||
public class AliasModel
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AliasModel.class);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -24,19 +24,19 @@ import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.action.AliasActionType;
|
||||
import io.github.dsheirer.alias.action.RecurringAction;
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.gui.preference.playback.ToneFrequency;
|
||||
import io.github.dsheirer.gui.preference.playback.ToneUtil;
|
||||
import io.github.dsheirer.gui.preference.playback.ToneVolume;
|
||||
import io.github.dsheirer.gui.preference.audio.playback.ToneFrequency;
|
||||
import io.github.dsheirer.gui.preference.audio.playback.ToneUtil;
|
||||
import io.github.dsheirer.gui.preference.audio.playback.ToneVolume;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
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;
|
||||
|
||||
public class BeepAction extends RecurringAction
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
@@ -89,7 +90,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
||||
{
|
||||
mAudioSegment.completeProperty().set(true);
|
||||
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
|
||||
mAudioSegment.decrementConsumerCount();
|
||||
mAudioSegment.removeLease(getClass().toString());
|
||||
mAudioSegment = null;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +113,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
||||
if(mAudioSegment == null)
|
||||
{
|
||||
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
|
||||
mAudioSegment.incrementConsumerCount();
|
||||
mAudioSegment.addLease(getClass().toString());
|
||||
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
|
||||
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
|
||||
|
||||
@@ -123,7 +124,6 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
||||
|
||||
if(mAudioSegmentListener != null)
|
||||
{
|
||||
mAudioSegment.incrementConsumerCount();
|
||||
mAudioSegmentListener.receive(mAudioSegment);
|
||||
}
|
||||
|
||||
@@ -136,26 +136,33 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
|
||||
|
||||
public void addAudio(float[] audioBuffer)
|
||||
{
|
||||
AudioSegment audioSegment = getAudioSegment();
|
||||
if(audioBuffer != null)
|
||||
{
|
||||
AudioSegment audioSegment = getAudioSegment();
|
||||
|
||||
//If the current segment exceeds the max samples length, close it so that a new segment gets generated
|
||||
//and then link the segments together
|
||||
if(mAudioSampleCount >= mMaxSegmentAudioSampleLength)
|
||||
{
|
||||
AudioSegment previous = getAudioSegment();
|
||||
closeAudioSegment();
|
||||
audioSegment = getAudioSegment();
|
||||
audioSegment.linkTo(previous);
|
||||
}
|
||||
//If the current segment exceeds the max samples length, close it so that a new segment gets generated
|
||||
//and then link the segments together
|
||||
if(mAudioSampleCount >= mMaxSegmentAudioSampleLength)
|
||||
{
|
||||
AudioSegment previous = getAudioSegment();
|
||||
closeAudioSegment();
|
||||
audioSegment = getAudioSegment();
|
||||
audioSegment.linkTo(previous);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
audioSegment.addAudio(audioBuffer);
|
||||
mAudioSampleCount += audioBuffer.length;
|
||||
try
|
||||
{
|
||||
audioSegment.addAudio(audioBuffer);
|
||||
mAudioSampleCount += audioBuffer.length;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
closeAudioSegment();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
else
|
||||
{
|
||||
closeAudioSegment();
|
||||
mLog.info("Attempt to add null audio from " + getClass());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
503
src/main/java/io/github/dsheirer/audio/AudioManager.java
Normal file
503
src/main/java/io/github/dsheirer/audio/AudioManager.java
Normal file
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
|
||||
import io.github.dsheirer.audio.call.ActiveAudioSegment;
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.audio.call.Call;
|
||||
import io.github.dsheirer.audio.call.CallRepository;
|
||||
import io.github.dsheirer.audio.call.ICallEventListener;
|
||||
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
|
||||
import io.github.dsheirer.audio.playbackfx.AudioPlaybackController;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.retention.AgeUnits;
|
||||
import io.github.dsheirer.preference.retention.RetentionPolicy;
|
||||
import io.github.dsheirer.preference.retention.RetentionPreference;
|
||||
import io.github.dsheirer.record.AudioRecordingManager;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Manages handling and distribution of audio segments and calls across the application.
|
||||
*/
|
||||
@Component("audioManager")
|
||||
public class AudioManager implements Listener<AudioSegment>
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AudioManager.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOGGER);
|
||||
private static final long GIGABYTES = 1024l * 1024l * 1024l;
|
||||
|
||||
@Resource
|
||||
private AudioPlaybackManager mAudioPlaybackManager;
|
||||
@Resource
|
||||
private AudioPlaybackController mAudioPlaybackController;
|
||||
@Resource
|
||||
private AudioRecordingManager mAudioRecordingManager;
|
||||
@Resource
|
||||
private AudioStreamingManager mAudioStreamingManager;
|
||||
@Resource
|
||||
private DuplicateCallDetector mDuplicateCallDetector;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
private CallRepository mCallRepository;
|
||||
private List<ICallEventListener> mCallListeners = new CopyOnWriteArrayList<>();
|
||||
private ActiveAudioSegmentProcessor mAudioSegmentProcessor = new ActiveAudioSegmentProcessor();
|
||||
private CallAgeOffProcessor mCallAgeOffProcessor = new CallAgeOffProcessor();
|
||||
private ScheduledFuture<?> mAudioSegmentProcessorFuture;
|
||||
private ScheduledFuture<?> mCallRetentionProcessorFuture;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
@Autowired
|
||||
public AudioManager(CallRepository callRepository)
|
||||
{
|
||||
mCallRepository = callRepository;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
LOGGER.info("Post constructing ......");
|
||||
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mAudioSegmentProcessor, 100,
|
||||
500, TimeUnit.MILLISECONDS);
|
||||
//Delay first run until 1-minute after startup.
|
||||
mCallRetentionProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mCallAgeOffProcessor,
|
||||
30, 30, TimeUnit.SECONDS);
|
||||
|
||||
//Register call event listeners
|
||||
add(mAudioRecordingManager);
|
||||
add(mAudioPlaybackController);
|
||||
add(mAudioStreamingManager);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void preDestroy()
|
||||
{
|
||||
LOGGER.info("AudioManager shutting down");
|
||||
|
||||
if(mAudioSegmentProcessorFuture != null)
|
||||
{
|
||||
mAudioSegmentProcessorFuture.cancel(true);
|
||||
mAudioSegmentProcessorFuture = null;
|
||||
}
|
||||
mAudioSegmentProcessor.shutdown();
|
||||
|
||||
if(mCallRetentionProcessorFuture != null)
|
||||
{
|
||||
mCallRetentionProcessorFuture.cancel(true);
|
||||
mCallRetentionProcessorFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the call listener to be notified of call events.
|
||||
* @param listener to add
|
||||
*/
|
||||
public void add(ICallEventListener listener)
|
||||
{
|
||||
if(!mCallListeners.contains(listener))
|
||||
{
|
||||
mCallListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call added event to registered listeners.
|
||||
* @param call that was added
|
||||
*/
|
||||
private void broadcastAdded(Call call)
|
||||
{
|
||||
for(ICallEventListener listener: mCallListeners)
|
||||
{
|
||||
listener.added(call);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call updated event to registered listeners.
|
||||
* @param call that was updated
|
||||
*/
|
||||
private void broadcastUpdated(Call call)
|
||||
{
|
||||
for(ICallEventListener listener: mCallListeners)
|
||||
{
|
||||
listener.updated(call);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call completed event to registered listeners.
|
||||
* @param call that was completed
|
||||
* @param audioSegment associated with the call.
|
||||
*/
|
||||
private void broadcastCompleted(Call call, AudioSegment audioSegment)
|
||||
{
|
||||
for(ICallEventListener listener: mCallListeners)
|
||||
{
|
||||
audioSegment.addLease(listener.getClass().toString());
|
||||
listener.completed(call, audioSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a call deleted event to registered listeners.
|
||||
* @param call that was deleted
|
||||
*/
|
||||
private void broadcastDeleted(Call call)
|
||||
{
|
||||
for(ICallEventListener listener: mCallListeners)
|
||||
{
|
||||
listener.deleted(call);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving call event notifications.
|
||||
* @param listener to remove
|
||||
*/
|
||||
public void remove(ICallEventListener listener)
|
||||
{
|
||||
mCallListeners.remove(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
//Let the duplicate call detector process the segment first to detect duplicates.
|
||||
audioSegment.addLease(mDuplicateCallDetector.getClass().toString());
|
||||
mDuplicateCallDetector.receive(audioSegment);
|
||||
audioSegment.addLease(mAudioSegmentProcessor.getClass().toString());
|
||||
mAudioSegmentProcessor.add(audioSegment);
|
||||
|
||||
//TODO: remove for cleanup
|
||||
// audioSegment.addLease(mAudioPlaybackManager.getClass().toString());
|
||||
// mAudioPlaybackManager.receive(audioSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Active audio segment processor. Single thread processor that runs as a scheduled task to process incoming
|
||||
* audio segments, update existing active audio segments, and process completed audio segments.
|
||||
*/
|
||||
private class ActiveAudioSegmentProcessor implements Runnable
|
||||
{
|
||||
private LinkedTransferQueue<AudioSegment> mQueuedAudioSegments = new LinkedTransferQueue<>();
|
||||
private List<AudioSegment> mAudioSegmentsToProcess = new ArrayList<>();
|
||||
private Map<AudioSegment,ActiveAudioSegment> mActiveAudioSegmentsMap = new HashMap<>();
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Adds the new audio segment to be processed
|
||||
* @param audioSegment to process
|
||||
*/
|
||||
public void add(AudioSegment audioSegment)
|
||||
{
|
||||
mQueuedAudioSegments.add(audioSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs shutdown and cleanup of any active audio segments that remain after the threaded processing
|
||||
* is terminated.
|
||||
*/
|
||||
public void shutdown()
|
||||
{
|
||||
for(Map.Entry<AudioSegment,ActiveAudioSegment> entry: mActiveAudioSegmentsMap.entrySet())
|
||||
{
|
||||
//Invoke dispose() method to remove the lease on the audio segment
|
||||
entry.getValue().dispose();
|
||||
}
|
||||
mActiveAudioSegmentsMap.clear();
|
||||
|
||||
for(AudioSegment audioSegment: mQueuedAudioSegments)
|
||||
{
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
mQueuedAudioSegments.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(mProcessing.compareAndSet(false, true))
|
||||
{
|
||||
//Process existing active audio segments
|
||||
try
|
||||
{
|
||||
Iterator<Map.Entry<AudioSegment,ActiveAudioSegment>> it = mActiveAudioSegmentsMap.entrySet().iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
ActiveAudioSegment activeAudioSegment = it.next().getValue();
|
||||
|
||||
if(activeAudioSegment.update())
|
||||
{
|
||||
broadcastUpdated(activeAudioSegment.getCall());
|
||||
}
|
||||
|
||||
if(activeAudioSegment.isComplete())
|
||||
{
|
||||
//Process residual audio and close the wave recording file.
|
||||
activeAudioSegment.closeAudio();
|
||||
|
||||
Call call = activeAudioSegment.getCall();
|
||||
call.setComplete(true);
|
||||
|
||||
try
|
||||
{
|
||||
mCallRepository.save(call);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGER.error("Couldn't update call in database");
|
||||
}
|
||||
broadcastCompleted(call, activeAudioSegment.getAudioSegment());
|
||||
activeAudioSegment.dispose();
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
LOGGER.error("Error while processing existing active audio segments", t);
|
||||
}
|
||||
|
||||
//Process new audio segments
|
||||
try
|
||||
{
|
||||
mQueuedAudioSegments.drainTo(mAudioSegmentsToProcess);
|
||||
|
||||
for(AudioSegment audioSegment: mAudioSegmentsToProcess)
|
||||
{
|
||||
if(mActiveAudioSegmentsMap.containsKey(audioSegment))
|
||||
{
|
||||
ActiveAudioSegment activeAudioSegment = mActiveAudioSegmentsMap.get(audioSegment);
|
||||
|
||||
if(activeAudioSegment.update())
|
||||
{
|
||||
broadcastUpdated(activeAudioSegment.getCall());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Call call = ActiveAudioSegment.create(audioSegment);
|
||||
ActiveAudioSegment activeAudioSegment = new ActiveAudioSegment(audioSegment, call, mUserPreferences);
|
||||
mActiveAudioSegmentsMap.put(audioSegment, activeAudioSegment);
|
||||
mCallRepository.save(call);
|
||||
broadcastAdded(call);
|
||||
}
|
||||
}
|
||||
|
||||
mAudioSegmentsToProcess.clear();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
LOGGER.error("Error while processing existing active audio segments", t);
|
||||
}
|
||||
|
||||
mProcessing.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call event retention and age-off processor.
|
||||
*/
|
||||
public class CallAgeOffProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
RetentionPreference retentionPreference = mUserPreferences.getRetentionPreference();
|
||||
RetentionPolicy policy = retentionPreference.getRetentionPolicy();
|
||||
|
||||
try
|
||||
{
|
||||
if(policy.isAgePolicy())
|
||||
{
|
||||
AgeUnits ageUnits = retentionPreference.getAgeUnits();
|
||||
int ageValue = retentionPreference.getAgeValue();
|
||||
long threshold = System.currentTimeMillis() - ageUnits.getDuration(ageValue);
|
||||
PageRequest requestPage1 = PageRequest.of(0, 100);
|
||||
long start = 0;
|
||||
Page<Call> page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, threshold, requestPage1);
|
||||
|
||||
while(page.getTotalElements() > 0)
|
||||
{
|
||||
List<Call> calls = page.get().collect(Collectors.toList());
|
||||
|
||||
for(Call call: calls)
|
||||
{
|
||||
//Keep adjusting the start time to 1 millisecond more than the current call so that
|
||||
//if there's an error deleting any given call, we don't get stuck processing it every time
|
||||
start = call.getEventTime() + 1;
|
||||
|
||||
//Only evaluate calls that are complete
|
||||
if(call.isComplete())
|
||||
{
|
||||
boolean deleteFailed = false;
|
||||
File file = new File(call.getFile());
|
||||
if(file.exists())
|
||||
{
|
||||
try
|
||||
{
|
||||
File parentDirectory = file.getParentFile();
|
||||
Files.delete(file.toPath());
|
||||
|
||||
//Delete parent directory if it is now empty
|
||||
if(parentDirectory.exists() && parentDirectory.isDirectory())
|
||||
{
|
||||
if(!Files.list(parentDirectory.toPath()).findFirst().isPresent())
|
||||
{
|
||||
Files.delete(parentDirectory.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception io)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("Call Event Delete", 5,
|
||||
"Error deleting call event recording: " + call.getFile() +
|
||||
" - " + io.getLocalizedMessage());
|
||||
deleteFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!deleteFailed)
|
||||
{
|
||||
mCallRepository.delete(call);
|
||||
broadcastDeleted(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, threshold, requestPage1);
|
||||
}
|
||||
}
|
||||
|
||||
if(policy.isSizePolicy())
|
||||
{
|
||||
Path callsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryCalls();
|
||||
|
||||
if(Files.exists(callsDirectory))
|
||||
{
|
||||
long currentSize = FileUtils.sizeOfDirectory(callsDirectory.toFile());
|
||||
long objectiveSize = retentionPreference.getObjectiveDirectorySize();
|
||||
|
||||
if(currentSize >= objectiveSize)
|
||||
{
|
||||
long start = 0;
|
||||
long now = System.currentTimeMillis();
|
||||
PageRequest pageable = PageRequest.of(0, 100);
|
||||
Page<Call> page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, now, pageable);
|
||||
|
||||
while(currentSize >= objectiveSize && page.getTotalElements() > 0)
|
||||
{
|
||||
List<Call> calls = page.get().collect(Collectors.toList());
|
||||
|
||||
for(Call call: calls)
|
||||
{
|
||||
//Keep adjusting the start time to 1 millisecond more than the current call so that
|
||||
//if there's an error deleting any given call, we don't get stuck processing it every time
|
||||
start = call.getEventTime() + 1;
|
||||
|
||||
if(currentSize < objectiveSize)
|
||||
{
|
||||
start = now - 1;
|
||||
break;
|
||||
}
|
||||
|
||||
//Only evaluate calls that are complete
|
||||
if(call.isComplete())
|
||||
{
|
||||
boolean deleteFailed = false;
|
||||
File file = new File(call.getFile());
|
||||
if(file.exists())
|
||||
{
|
||||
try
|
||||
{
|
||||
File parentDirectory = file.getParentFile();
|
||||
long fileSize = FileUtils.sizeOf(file);
|
||||
Files.delete(file.toPath());
|
||||
currentSize -= fileSize;
|
||||
|
||||
//Delete parent directory if it is now empty
|
||||
if(parentDirectory.exists() && parentDirectory.isDirectory())
|
||||
{
|
||||
if(!Files.list(parentDirectory.toPath()).findFirst().isPresent())
|
||||
{
|
||||
Files.delete(parentDirectory.toPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception io)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("Call Event Delete", 5,
|
||||
"Error deleting call event recording: " + call.getFile() +
|
||||
" - " + io.getLocalizedMessage());
|
||||
deleteFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!deleteFailed)
|
||||
{
|
||||
mCallRepository.delete(call);
|
||||
broadcastDeleted(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, now, pageable);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
LOGGER.error("Unexpected error while deleting call events using retention policy: " + policy, t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
@@ -31,6 +32,8 @@ import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.duplicate.CallManagementPreference;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -41,6 +44,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
|
||||
@@ -48,15 +52,26 @@ import org.slf4j.LoggerFactory;
|
||||
* Note: system in this context refers to the system name value that is used in channel configurations. All decoder
|
||||
* channels must share the same system name for call duplication detection.
|
||||
*/
|
||||
@Component("duplicateCallDetector")
|
||||
public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
|
||||
private CallManagementPreference mCallManagementPreference;
|
||||
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
public DuplicateCallDetector(UserPreferences userPreferences)
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public DuplicateCallDetector()
|
||||
{
|
||||
mCallManagementPreference = userPreferences.getDuplicateCallDetectionPreference();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
mCallManagementPreference = mUserPreferences.getCallManagementPreference();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,7 +96,9 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
mDetectorMap.put(system, detector);
|
||||
}
|
||||
|
||||
audioSegment.addLease(detector.getClass().toString());
|
||||
detector.add(audioSegment);
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,7 +258,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
|
||||
if(complete)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
|
||||
return complete;
|
||||
@@ -270,7 +287,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
if(isDuplicate(current, toCheck))
|
||||
{
|
||||
toCheck.setDuplicate(true);
|
||||
toCheck.decrementConsumerCount();
|
||||
toCheck.removeLease(getClass().toString());
|
||||
duplicates.add(toCheck);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,10 @@ package io.github.dsheirer.audio.broadcast;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.audio.call.Call;
|
||||
import io.github.dsheirer.audio.call.CompletedCall;
|
||||
import io.github.dsheirer.audio.call.ICallEventListener;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
@@ -32,14 +35,15 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioSegmentRecorder;
|
||||
import io.github.dsheirer.record.RecordFormat;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
@@ -47,59 +51,50 @@ import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on
|
||||
* disk and enqueues the temporary recording for streaming.
|
||||
*/
|
||||
public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
@Component("audioStreamingManager")
|
||||
public class AudioStreamingManager implements ICallEventListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingManager.class);
|
||||
private LinkedTransferQueue<AudioSegment> mNewAudioSegments = new LinkedTransferQueue<>();
|
||||
private List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private Listener<AudioRecording> mAudioRecordingListener;
|
||||
private BroadcastFormat mBroadcastFormat;
|
||||
@Resource
|
||||
private BroadcastModel mBroadcastModel;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
private LinkedTransferQueue<CompletedCall> mCompletedCallQueue = new LinkedTransferQueue<>();
|
||||
private List<CompletedCall> mCompletedCalls = new ArrayList<>();
|
||||
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
|
||||
private ScheduledFuture<?> mAudioSegmentProcessorFuture;
|
||||
private int mNextRecordingNumber = 1;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param listener to receive completed audio recordings
|
||||
* @param broadcastFormat for temporary recordings
|
||||
* @param userPreferences to manage recording directories
|
||||
*/
|
||||
public AudioStreamingManager(Listener<AudioRecording> listener, BroadcastFormat broadcastFormat, UserPreferences userPreferences)
|
||||
public AudioStreamingManager()
|
||||
{
|
||||
mAudioRecordingListener = listener;
|
||||
mBroadcastFormat = broadcastFormat;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary receive method
|
||||
*/
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
mNewAudioSegments.add(audioSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the scheduled audio segment processor
|
||||
*/
|
||||
@PostConstruct
|
||||
public void start()
|
||||
{
|
||||
if(mAudioSegmentProcessorFuture == null)
|
||||
{
|
||||
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 250, TimeUnit.MILLISECONDS);
|
||||
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new CompletedCallProcessor(),
|
||||
0, 250, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the scheduled audio segment processor
|
||||
*/
|
||||
@PreDestroy
|
||||
public void stop()
|
||||
{
|
||||
if(mAudioSegmentProcessorFuture != null)
|
||||
@@ -108,51 +103,60 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
mAudioSegmentProcessorFuture = null;
|
||||
}
|
||||
|
||||
for(AudioSegment audioSegment: mNewAudioSegments)
|
||||
for(CompletedCall completedCall: mCompletedCallQueue)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
completedCall.audioSegment().removeLease(getClass().toString());
|
||||
}
|
||||
|
||||
mNewAudioSegments.clear();
|
||||
mCompletedCallQueue.clear();
|
||||
|
||||
for(AudioSegment audioSegment: mAudioSegments)
|
||||
for(CompletedCall completedCall: mCompletedCalls)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
completedCall.audioSegment().removeLease(getClass().toString());
|
||||
}
|
||||
|
||||
mAudioSegments.clear();
|
||||
mCompletedCalls.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the ICallEventListener interface to receive and enqueue completed calls to process for streaming.
|
||||
* @param call that was completed.
|
||||
* @param audioSegment for the call
|
||||
*/
|
||||
@Override
|
||||
public void completed(Call call, AudioSegment audioSegment)
|
||||
{
|
||||
mCompletedCallQueue.add(new CompletedCall(call, audioSegment));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(Call call) {} //Not implemented
|
||||
@Override
|
||||
public void updated(Call call) {} //Not implemented
|
||||
@Override
|
||||
public void deleted(Call call) {} //Not implemented
|
||||
|
||||
/**
|
||||
* Main processing method to process audio segments
|
||||
*/
|
||||
private void processAudioSegments()
|
||||
private void processCompletedCalls()
|
||||
{
|
||||
mNewAudioSegments.drainTo(mAudioSegments);
|
||||
CompletedCall completedCall = mCompletedCallQueue.poll();
|
||||
|
||||
Iterator<AudioSegment> it = mAudioSegments.iterator();
|
||||
AudioSegment audioSegment;
|
||||
while(it.hasNext())
|
||||
while(completedCall != null)
|
||||
{
|
||||
audioSegment = it.next();
|
||||
AudioSegment audioSegment = completedCall.audioSegment();
|
||||
|
||||
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
|
||||
if(!(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled()))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
else if(audioSegment.completeProperty().get())
|
||||
{
|
||||
it.remove();
|
||||
|
||||
if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
|
||||
if(mBroadcastModel != null && audioSegment.hasBroadcastChannels())
|
||||
{
|
||||
IdentifierCollection identifiers =
|
||||
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
|
||||
|
||||
if(identifiers.getToIdentifier() instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
if(mUserPreferences.getDuplicateCallDetectionPreference()
|
||||
if(mUserPreferences.getCallManagementPreference()
|
||||
.getPatchGroupStreamingOption() == PatchGroupStreamingOption.TALKGROUPS)
|
||||
{
|
||||
//Decompose the patch group into the individual (patched) talkgroups and process the audio
|
||||
@@ -203,9 +207,10 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
}
|
||||
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
completedCall = mCompletedCallQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +240,7 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, broadcastChannels, identifierCollection,
|
||||
audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
mBroadcastModel.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
@@ -269,20 +274,20 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled runnable to process audio segments.
|
||||
* Scheduled runnable to process completed calls.
|
||||
*/
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
public class CompletedCallProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
processAudioSegments();
|
||||
processCompletedCalls();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error processing audio segments for streaming", t);
|
||||
mLog.error("Error processing completed calls for streaming", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -22,16 +22,14 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2AudioStreamingBroadcaster;
|
||||
@@ -65,29 +63,26 @@ public class BroadcastFactory
|
||||
switch(configuration.getBroadcastServerType())
|
||||
{
|
||||
case BROADCASTIFY_CALL:
|
||||
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration);
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration);
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
inputAudioFormat, mp3Setting);
|
||||
case ICECAST_TCP:
|
||||
return new IcecastTCPAudioBroadcaster((IcecastTCPConfiguration) configuration, inputAudioFormat,
|
||||
mp3Setting, aliasModel);
|
||||
mp3Setting);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPAudioBroadcaster((IcecastHTTPConfiguration) configuration, inputAudioFormat,
|
||||
mp3Setting, aliasModel);
|
||||
mp3Setting);
|
||||
case SHOUTCAST_V1:
|
||||
return new ShoutcastV1AudioBroadcaster((ShoutcastV1Configuration) configuration, inputAudioFormat,
|
||||
mp3Setting, aliasModel);
|
||||
mp3Setting);
|
||||
case SHOUTCAST_V2:
|
||||
return new ShoutcastV2AudioStreamingBroadcaster((ShoutcastV2Configuration) configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
inputAudioFormat, mp3Setting);
|
||||
case UNKNOWN:
|
||||
default:
|
||||
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -20,12 +20,13 @@ package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
@@ -45,9 +46,11 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
|
||||
@Component("broadcastModel")
|
||||
public class BroadcastModel extends AbstractTableModel implements Listener<AudioRecording>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
|
||||
@@ -68,28 +71,30 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
public static final String[] COLUMN_NAMES = new String[]
|
||||
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
|
||||
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
|
||||
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
|
||||
private List<AudioRecording> mRecordingQueue = new CopyOnWriteArrayList<>();
|
||||
private Map<Integer,AbstractAudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
||||
private IconModel mIconModel;
|
||||
private AliasModel mAliasModel;
|
||||
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
|
||||
private BroadcastEventListener mBroadcastEventListener = new BroadcastEventListener();
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
/**
|
||||
* Model for managing Broadcast configurations and any associated broadcaster instances.
|
||||
*/
|
||||
public BroadcastModel(AliasModel aliasModel, IconModel iconModel, UserPreferences userPreferences)
|
||||
public BroadcastModel()
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
mIconModel = iconModel;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
|
||||
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS);
|
||||
|
||||
removeOrphanedTemporaryRecordings();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -31,9 +31,9 @@ public enum BroadcastServerType
|
||||
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
|
||||
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||
UNKNOWN("Unknown", null);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -20,10 +20,12 @@ package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.icon.Icon;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.Dimension;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
@@ -42,23 +44,15 @@ public class BroadcastStatusPanel extends JPanel
|
||||
private JTable mTable;
|
||||
private JTableColumnWidthMonitor mColumnWidthMonitor;
|
||||
private JScrollPane mScrollPane;
|
||||
@Resource
|
||||
private BroadcastModel mBroadcastModel;
|
||||
private UserPreferences mUserPreferences;
|
||||
private String mPreferenceKey;
|
||||
private String mPreferenceKey = "application.broadcast.status.panel";
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param broadcastModel to access the streams
|
||||
* @param userPreferences for configuring the panel
|
||||
* @param preferenceKey to store column preferences for this panel.
|
||||
*/
|
||||
public BroadcastStatusPanel(BroadcastModel broadcastModel, UserPreferences userPreferences, String preferenceKey)
|
||||
public BroadcastStatusPanel()
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
mUserPreferences = userPreferences;
|
||||
mPreferenceKey = preferenceKey;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public JTable getTable()
|
||||
@@ -66,21 +60,19 @@ public class BroadcastStatusPanel extends JPanel
|
||||
return mTable;
|
||||
}
|
||||
|
||||
private void init()
|
||||
@PostConstruct
|
||||
private void postConstruct()
|
||||
{
|
||||
setPreferredSize(new Dimension(880, 70));
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
|
||||
|
||||
mTable = new JTable(mBroadcastModel);
|
||||
|
||||
mTable.setEnabled(false);
|
||||
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class);
|
||||
renderer.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS).setCellRenderer(new StatusCellRenderer());
|
||||
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCAST_SERVER_TYPE).setCellRenderer(new ServerTypeRenderer());
|
||||
mColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, mPreferenceKey);
|
||||
|
||||
mColumnWidthMonitor = new JTableColumnWidthMonitor(mTable, mPreferenceKey);
|
||||
mScrollPane = new JScrollPane(mTable);
|
||||
|
||||
add(mScrollPane);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,13 +20,10 @@
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
@@ -80,10 +77,8 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
* @param config to use
|
||||
* @param aliasModel for access to aliases
|
||||
*/
|
||||
public BroadcastifyCallBroadcaster(BroadcastifyCallConfiguration config, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
public BroadcastifyCallBroadcaster(BroadcastifyCallConfiguration config)
|
||||
{
|
||||
super(config);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
@@ -28,13 +27,11 @@ import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
|
||||
{
|
||||
private IBroadcastMetadataUpdater mMetadataUpdater;
|
||||
protected AliasModel mAliasModel;
|
||||
|
||||
public IcecastAudioBroadcaster(BroadcastConfiguration broadcastConfiguration, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
MP3Setting mp3Setting)
|
||||
{
|
||||
super(broadcastConfiguration, inputAudioFormat, mp3Setting);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +47,7 @@ public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
|
||||
{
|
||||
if(mMetadataUpdater == null)
|
||||
{
|
||||
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration(), mAliasModel);
|
||||
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration());
|
||||
}
|
||||
|
||||
return mMetadataUpdater;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -18,23 +18,12 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastMetadata;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
@@ -42,7 +31,8 @@ import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
|
||||
{
|
||||
@@ -50,6 +40,7 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
|
||||
private final static String UTF8 = "UTF-8";
|
||||
private HttpClient mHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
|
||||
private IcecastConfiguration mIcecastConfiguration;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
private boolean mConnectionLoggingSuppressed = false;
|
||||
|
||||
@@ -59,10 +50,9 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
|
||||
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
|
||||
* sequence, those updates will be queued and processed in the order received.
|
||||
*/
|
||||
public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration, AliasModel aliasModel)
|
||||
public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration)
|
||||
{
|
||||
mIcecastConfiguration = icecastConfiguration;
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -26,6 +26,7 @@ import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
@@ -54,7 +55,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
private static final String HTTP_1_0_OK_HEX_DUMP = "48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B";
|
||||
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
private Map<String,String> mHTTPHeaders;
|
||||
@@ -74,9 +76,9 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
* @param configuration for the Icecast stream
|
||||
*/
|
||||
public IcecastHTTPAudioBroadcaster(IcecastHTTPConfiguration configuration, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
MP3Setting mp3Setting)
|
||||
{
|
||||
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
|
||||
super(configuration, inputAudioFormat, mp3Setting);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
@@ -51,7 +52,8 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
|
||||
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds
|
||||
private static final int WRITE_TIMEOUT_SECONDS = 5;
|
||||
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
|
||||
@@ -70,9 +72,9 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
* @param configuration for the Icecast stream
|
||||
*/
|
||||
public IcecastTCPAudioBroadcaster(IcecastTCPConfiguration configuration, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
MP3Setting mp3Setting)
|
||||
{
|
||||
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
|
||||
super(configuration, inputAudioFormat, mp3Setting);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -19,55 +19,43 @@
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
|
||||
@@ -93,18 +81,16 @@
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
* @param config to use
|
||||
* @param aliasModel for access to aliases
|
||||
*/
|
||||
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
public OpenMHzBroadcaster(OpenMHzConfiguration config)
|
||||
{
|
||||
super(config);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,7 +577,7 @@
|
||||
return "No Response";
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -17,24 +17,18 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
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.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.gui.playlist.streaming.AbstractBroadcastEditor;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* OpenMHz calls API configuration editor
|
||||
@@ -50,11 +44,9 @@
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for accessing the broadcast model
|
||||
*/
|
||||
public OpenMHzEditor(PlaylistManager playlistManager)
|
||||
public OpenMHzEditor()
|
||||
{
|
||||
super(playlistManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,8 +27,6 @@ import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
@@ -40,6 +38,7 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
@@ -78,18 +77,16 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
* @param config to use
|
||||
* @param aliasModel for access to aliases
|
||||
*/
|
||||
public RdioScannerBroadcaster(RdioScannerConfiguration config, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
public RdioScannerBroadcaster(RdioScannerConfiguration config)
|
||||
{
|
||||
super(config);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -440,7 +437,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
*/
|
||||
private String getTalkgroupLabel(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
@@ -465,7 +461,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
*/
|
||||
private String getTalkgroupGroup(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
|
||||
@@ -26,6 +25,10 @@ import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
@@ -37,11 +40,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1AudioBroadcaster.class);
|
||||
@@ -50,7 +48,6 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
private IBroadcastMetadataUpdater mMetadataUpdater;
|
||||
private AliasModel mAliasModel;
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
|
||||
@@ -65,10 +62,9 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
|
||||
* @param configuration for the Shoutcast stream
|
||||
*/
|
||||
public ShoutcastV1AudioBroadcaster(ShoutcastV1Configuration configuration, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
MP3Setting mp3Setting)
|
||||
{
|
||||
super(configuration, inputAudioFormat, mp3Setting);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +80,7 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
|
||||
{
|
||||
if(mMetadataUpdater == null)
|
||||
{
|
||||
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getConfiguration(), mAliasModel);
|
||||
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getConfiguration());
|
||||
}
|
||||
|
||||
return mMetadataUpdater;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -29,6 +29,17 @@ import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
@@ -41,23 +52,13 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
|
||||
private final static String UTF8 = "UTF-8";
|
||||
|
||||
private ShoutcastV1Configuration mShoutcastV1Configuration;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private AtomicBoolean mUpdating = new AtomicBoolean();
|
||||
@@ -70,10 +71,9 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
|
||||
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
|
||||
* sequence, those updates will be queued and processed in the order received.
|
||||
*/
|
||||
public ShoutcastV1BroadcastMetadataUpdater(ShoutcastV1Configuration shoutcastV1Configuration, AliasModel aliasModel)
|
||||
public ShoutcastV1BroadcastMetadataUpdater(ShoutcastV1Configuration shoutcastV1Configuration)
|
||||
{
|
||||
mShoutcastV1Configuration = shoutcastV1Configuration;
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -49,6 +49,15 @@ import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
@@ -59,25 +68,17 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class);
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
private int mMaxPayloadSize = 16377;
|
||||
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
private AliasModel mAliasModel;
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private int mMaxPayloadSize = 16377;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
private LinkedTransferQueue<UltravoxMessage> mMetadataMessageQueue = new LinkedTransferQueue<>();
|
||||
|
||||
@@ -94,10 +95,9 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas
|
||||
* @param configuration for the Shoutcast V2 stream
|
||||
*/
|
||||
public ShoutcastV2AudioStreamingBroadcaster(ShoutcastV2Configuration configuration, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
MP3Setting mp3Setting)
|
||||
{
|
||||
super(configuration, inputAudioFormat, mp3Setting);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
public ShoutcastV2Configuration getConfiguration()
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.call;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ChannelNameConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.wave.AudioMetadata;
|
||||
import io.github.dsheirer.record.wave.AudioMetadataUtils;
|
||||
import io.github.dsheirer.record.wave.WaveWriter;
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Manages an active audio segment through to completion and provides options for refreshing the associated call
|
||||
* object.
|
||||
*/
|
||||
public class ActiveAudioSegment
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(ActiveAudioSegment.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOGGER);
|
||||
private static final SimpleDateFormat DIRECTORY_SDF = new SimpleDateFormat("yyyyMMdd");
|
||||
private static final SimpleDateFormat FILE_NAME_SDF = new SimpleDateFormat("yyyyMMdd_HHmmss");
|
||||
private static int sFileNameCounter = 1;
|
||||
|
||||
private AudioSegment mAudioSegment;
|
||||
private Call mCall;
|
||||
private UserPreferences mUserPreferences;
|
||||
private WaveWriter mWaveWriter;
|
||||
private Path mPath;
|
||||
private int mAudioBufferPointer = 0;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param audioSegment
|
||||
* @param call
|
||||
* @param userPreferences for lookup of recording directory
|
||||
*/
|
||||
public ActiveAudioSegment(AudioSegment audioSegment, Call call, UserPreferences userPreferences)
|
||||
{
|
||||
mAudioSegment = audioSegment;
|
||||
mAudioSegment.addLease(getClass().toString());
|
||||
mCall = call;
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
mPath = getAudioRecordingPath();
|
||||
mCall.setFile(mPath.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs disposal workflow.
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
if(mWaveWriter != null)
|
||||
{
|
||||
closeAudio();
|
||||
}
|
||||
mCall.setComplete(true);
|
||||
mAudioSegment.removeLease(getClass().toString());
|
||||
mAudioSegment = null;
|
||||
mCall = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio Segment that is active.
|
||||
* @return audio segment
|
||||
*/
|
||||
public AudioSegment getAudioSegment()
|
||||
{
|
||||
return mAudioSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call entity associated with this audio segment
|
||||
* @return call entity
|
||||
*/
|
||||
public Call getCall()
|
||||
{
|
||||
return mCall;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment is active and continues to receive audio buffer and identifier updates.
|
||||
* @return true if active or false if the audio segment is complete.
|
||||
*/
|
||||
public boolean isComplete()
|
||||
{
|
||||
return getAudioSegment().isComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to update/refresh the call entity with details from the audio segment and returns true if any details in
|
||||
* the call entity were updated or false otherwise.
|
||||
* @return status of the update, true if updated or false if no update.
|
||||
*/
|
||||
public boolean update()
|
||||
{
|
||||
boolean updated = false;
|
||||
updated |= updateTo(mCall, mAudioSegment.getIdentifierCollection().getToIdentifier(), getAudioSegment().getAliasList());
|
||||
updated |= updateFrom(mCall, mAudioSegment.getIdentifierCollection().getFromIdentifier(), getAudioSegment().getAliasList());
|
||||
updated |= updateDuration(mCall, mAudioSegment.getDuration());
|
||||
updated |= updateFlags(mCall, mAudioSegment);
|
||||
updated |= processAudio();
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique audio recording file path.
|
||||
* @return audio recording path.
|
||||
*/
|
||||
private Path getAudioRecordingPath()
|
||||
{
|
||||
Date now = new Date();
|
||||
String dailyFolder = DIRECTORY_SDF.format(now);
|
||||
Path recordingDirectory = mUserPreferences.getDirectoryPreference().getDirectoryCalls()
|
||||
.resolve(dailyFolder);
|
||||
|
||||
if(!Files.exists(recordingDirectory))
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.createDirectories(recordingDirectory);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error(dailyFolder, 1, "Unable to create daily calls recording " +
|
||||
"folder [" + dailyFolder + "]", ioe);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int recordingNumber = sFileNameCounter++;
|
||||
String fileName = FILE_NAME_SDF.format(now) + "_call_" + recordingNumber + ".wav";
|
||||
return recordingDirectory.resolve(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes any buffered audio to the wave recording.
|
||||
* @return true if the call was updated with the audio recording path.
|
||||
*/
|
||||
private boolean processAudio()
|
||||
{
|
||||
boolean updated = false;
|
||||
|
||||
if(mAudioSegment.hasAudio())
|
||||
{
|
||||
if(mWaveWriter == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
mWaveWriter = new WaveWriter(AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO, mPath);
|
||||
updated = true;
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
LOGGER.error("Error creating wave file: " + mPath);
|
||||
}
|
||||
}
|
||||
|
||||
List<float[]> audioBuffers = mAudioSegment.getAudioBuffers();
|
||||
int size = audioBuffers.size();
|
||||
|
||||
if(mWaveWriter != null && size > mAudioBufferPointer)
|
||||
{
|
||||
for(int x = mAudioBufferPointer; x < audioBuffers.size(); x++)
|
||||
{
|
||||
try
|
||||
{
|
||||
mWaveWriter.writeData(ConversionUtils.convertToSigned16BitSamples(audioBuffers.get(x)));
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("Call Wave Writer", 3, "Error while recording call audio", ioe);
|
||||
}
|
||||
}
|
||||
|
||||
mAudioBufferPointer = size;
|
||||
}
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes remaining audio and metadata to wave file and closes the recorder.
|
||||
*/
|
||||
public void closeAudio()
|
||||
{
|
||||
processAudio();
|
||||
|
||||
if(mWaveWriter != null)
|
||||
{
|
||||
Map<AudioMetadata,String> metadataMap = AudioMetadataUtils.getMetadataMap(mAudioSegment.getIdentifierCollection(),
|
||||
mAudioSegment.getAliasList());
|
||||
|
||||
ByteBuffer listChunk = AudioMetadataUtils.getLISTChunk(metadataMap);
|
||||
byte[] id3Bytes = AudioMetadataUtils.getMP3ID3(metadataMap);
|
||||
ByteBuffer id3Chunk = AudioMetadataUtils.getID3Chunk(id3Bytes);
|
||||
try
|
||||
{
|
||||
mWaveWriter.writeMetadata(listChunk, id3Chunk);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("Metadata Write Error", 3,
|
||||
"Error writing call ID3 metadata to recording", ioe);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mWaveWriter.close();
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("Recording Close Error", 3, "Error closing audio wave recording", ioe);
|
||||
}
|
||||
|
||||
mWaveWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to create an initial call instance from an Audio Segment. Note: this static method is used by
|
||||
* an external database controller so that the call event can be stored in the database before
|
||||
* @param audioSegment
|
||||
* @return
|
||||
*/
|
||||
public static Call create(AudioSegment audioSegment)
|
||||
{
|
||||
if(audioSegment == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Call call = new Call();
|
||||
|
||||
call.setEventTime(audioSegment.getStartTimestamp());
|
||||
|
||||
Identifier decoder = audioSegment.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
|
||||
Form.DECODER_TYPE, Role.ANY);
|
||||
|
||||
if(decoder instanceof DecoderTypeConfigurationIdentifier dtci)
|
||||
{
|
||||
call.setProtocol(dtci.getValue().toString());
|
||||
}
|
||||
|
||||
Identifier systemId = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
|
||||
|
||||
if(systemId instanceof SystemConfigurationIdentifier sci)
|
||||
{
|
||||
call.setSystem(sci.getValue());
|
||||
}
|
||||
|
||||
Identifier siteId = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SITE, Role.ANY);
|
||||
|
||||
if(siteId instanceof SiteConfigurationIdentifier siteci)
|
||||
{
|
||||
call.setSite(siteci.getValue());
|
||||
}
|
||||
|
||||
Identifier channelId = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL, Role.ANY);
|
||||
|
||||
if(channelId instanceof ChannelNameConfigurationIdentifier cnci)
|
||||
{
|
||||
call.setChannel(cnci.getValue());
|
||||
}
|
||||
|
||||
Identifier frequency = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL_FREQUENCY, Role.ANY);
|
||||
|
||||
if(frequency instanceof FrequencyConfigurationIdentifier fci)
|
||||
{
|
||||
//Convert Hertz to MegaHertz for the call value.
|
||||
call.setFrequency(fci.getValue() / 1E6d);
|
||||
}
|
||||
|
||||
updateFrom(call, audioSegment.getIdentifierCollection().getFromIdentifier(), audioSegment.getAliasList());
|
||||
updateTo(call, audioSegment.getIdentifierCollection().getToIdentifier(), audioSegment.getAliasList());
|
||||
updateDuration(call, audioSegment.getDuration());
|
||||
updateFlags(call, audioSegment);
|
||||
|
||||
return call;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the duplicate, record, stream and other call flags/info
|
||||
* @param call to update
|
||||
* @param audioSegment containing flags/info
|
||||
* @return true if the call is updated.
|
||||
*/
|
||||
private static boolean updateFlags(Call call, AudioSegment audioSegment)
|
||||
{
|
||||
boolean updated = false;
|
||||
|
||||
if(call.isDuplicate() ^ audioSegment.isDuplicate())
|
||||
{
|
||||
call.setDuplicate(true);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if(call.isRecord() ^ audioSegment.recordAudioProperty().get())
|
||||
{
|
||||
call.setRecord(true);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if(call.isStream() ^ audioSegment.hasBroadcastChannels())
|
||||
{
|
||||
call.setStream(true);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if(call.getMonitor() != audioSegment.monitorPriorityProperty().get())
|
||||
{
|
||||
call.setMonitor(audioSegment.monitorPriorityProperty().get());
|
||||
updated = true;
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the call duration field
|
||||
* @param call to update
|
||||
* @param durationMs value
|
||||
* @return true if the value was updated
|
||||
*/
|
||||
private static boolean updateDuration(Call call, long durationMs)
|
||||
{
|
||||
double durationSeconds = durationMs / 1000.0;
|
||||
|
||||
if(call.getDuration() != durationSeconds)
|
||||
{
|
||||
call.setDuration(durationSeconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the FROM identifier value.
|
||||
* @param call to update
|
||||
* @param fromIdentifier value
|
||||
* @param aliasList for alias lookups
|
||||
* @return true if the value was updated
|
||||
*/
|
||||
private static boolean updateFrom(Call call, Identifier fromIdentifier, AliasList aliasList)
|
||||
{
|
||||
if(fromIdentifier == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(call.getFromId() == null || call.getFromId().contentEquals(fromIdentifier.toString()))
|
||||
{
|
||||
call.setFromId(fromIdentifier.toString());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(fromIdentifier);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
call.setFromAlias(Joiner.on(";").join(aliases));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the TO identifier value.
|
||||
* @param call to update
|
||||
* @param toIdentifier value
|
||||
* @return true if the value was updated
|
||||
*/
|
||||
private static boolean updateTo(Call call, Identifier toIdentifier, AliasList aliasList)
|
||||
{
|
||||
if(toIdentifier == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if(call.getToId() == null || call.getToId().contentEquals(toIdentifier.toString()))
|
||||
{
|
||||
call.setToId(toIdentifier.toString());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(toIdentifier);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
call.setToAlias(Joiner.on(";").join(aliases));
|
||||
}
|
||||
}
|
||||
|
||||
switch(toIdentifier.getForm())
|
||||
{
|
||||
case TALKGROUP -> call.setCallType("Talk Group");
|
||||
case RADIO ->
|
||||
{
|
||||
LOGGER.info("Private Call detected - TO identifier: " + toIdentifier);
|
||||
call.setCallType("Private");
|
||||
}
|
||||
case PATCH_GROUP -> call.setCallType("Patch Group");
|
||||
default -> call.setCallType(toIdentifier.getForm().toString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.call;
|
||||
|
||||
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelController;
|
||||
import io.github.dsheirer.audio.playbackfx.IAudioPlaybackStatusListener;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.paint.Color;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.javafx.IconNode;
|
||||
|
||||
/**
|
||||
* Audio playback controller.
|
||||
*/
|
||||
public class AudioPlaybackControlView extends HBox implements IAudioPlaybackStatusListener
|
||||
{
|
||||
private static final int ICON_SIZE = 16;
|
||||
private ObjectProperty<MediaPlayer> mMediaPlayer = new SimpleObjectProperty<>();
|
||||
private ToggleButton mAutoRepeatButton;
|
||||
private ToggleGroup mPlaybackGroup;
|
||||
private ToggleButton mPlayButton;
|
||||
private ToggleButton mPauseButton;
|
||||
private ToggleButton mStopButton;
|
||||
private boolean mUpdating = false;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public AudioPlaybackControlView()
|
||||
{
|
||||
setSpacing(2);
|
||||
Separator separator = new Separator(Orientation.VERTICAL);
|
||||
separator.setPadding(new Insets(0, 5, 0 , 7));
|
||||
getChildren().addAll(getStopButton(), getPlayButton(), getPauseButton(), getAutoRepeatButton());
|
||||
mediaPlayerProperty().addListener((observable, oldValue, newValue) -> updateControls());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the IAudioPLaybackStatusListener method to click the stop button when end of media is detected.
|
||||
* @param controller where this occurred.
|
||||
*/
|
||||
@Override
|
||||
public void endOfMedia(AudioPlaybackChannelController controller)
|
||||
{
|
||||
getStopButton().setSelected(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus,
|
||||
MediaPlayer.Status currentStatus)
|
||||
{
|
||||
if(currentStatus != null)
|
||||
{
|
||||
switch(currentStatus)
|
||||
{
|
||||
case UNKNOWN:
|
||||
break;
|
||||
case READY:
|
||||
break;
|
||||
case PAUSED:
|
||||
mUpdating = true;
|
||||
getPauseButton().setSelected(true);
|
||||
mUpdating = false;
|
||||
break;
|
||||
case PLAYING:
|
||||
mUpdating = true;
|
||||
getPlayButton().setSelected(true);
|
||||
mUpdating = false;
|
||||
break;
|
||||
case STOPPED:
|
||||
mUpdating = true;
|
||||
getStopButton().setSelected(true);
|
||||
mUpdating = false;
|
||||
if(getAutoRepeatButton().isSelected())
|
||||
{
|
||||
getPlayButton().setSelected(true);
|
||||
}
|
||||
break;
|
||||
case STALLED:
|
||||
case HALTED:
|
||||
case DISPOSED:
|
||||
System.out.println("Playback is " + currentStatus);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the controls when the media player changes
|
||||
*/
|
||||
private void updateControls()
|
||||
{
|
||||
boolean disable = mediaPlayerProperty().get() == null;
|
||||
getStopButton().setDisable(disable);
|
||||
getPlayButton().setDisable(disable);
|
||||
getPauseButton().setDisable(disable);
|
||||
getAutoRepeatButton().setDisable(disable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Media player property that can be bound to a media player controller.
|
||||
*/
|
||||
public ObjectProperty<MediaPlayer> mediaPlayerProperty()
|
||||
{
|
||||
return mMediaPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback toggle buttons group (play, stop, and pause)
|
||||
*/
|
||||
private ToggleGroup getPlaybackGroup()
|
||||
{
|
||||
if(mPlaybackGroup == null)
|
||||
{
|
||||
mPlaybackGroup = new ToggleGroup();
|
||||
mPlaybackGroup.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> {
|
||||
MediaPlayer mediaPlayer = mediaPlayerProperty().get();
|
||||
|
||||
if(!mUpdating && mediaPlayer != null)
|
||||
{
|
||||
if(getPlayButton().equals(selectedToggle))
|
||||
{
|
||||
mediaPlayer.play();
|
||||
}
|
||||
else if(getPauseButton().equals(selectedToggle))
|
||||
{
|
||||
mediaPlayer.pause();
|
||||
}
|
||||
else if(getStopButton().equals(selectedToggle))
|
||||
{
|
||||
mediaPlayer.stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mPlaybackGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play button
|
||||
*/
|
||||
private ToggleButton getPlayButton()
|
||||
{
|
||||
if(mPlayButton == null)
|
||||
{
|
||||
mPlayButton = new ToggleButton();
|
||||
mPlayButton.setToggleGroup(getPlaybackGroup());
|
||||
mPlayButton.setTooltip(new Tooltip("Play Audio"));
|
||||
mPlayButton.setDisable(true);
|
||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mPlayButton.setGraphic(iconNode);
|
||||
}
|
||||
|
||||
return mPlayButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause button
|
||||
*/
|
||||
private ToggleButton getPauseButton()
|
||||
{
|
||||
if(mPauseButton == null)
|
||||
{
|
||||
mPauseButton = new ToggleButton();
|
||||
mPauseButton.setToggleGroup(getPlaybackGroup());
|
||||
mPauseButton.setTooltip(new Tooltip("Pause Audio Playback"));
|
||||
mPauseButton.setDisable(true);
|
||||
IconNode iconNode = new IconNode(FontAwesome.PAUSE);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mPauseButton.setGraphic(iconNode);
|
||||
}
|
||||
|
||||
return mPauseButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop button
|
||||
*/
|
||||
private ToggleButton getStopButton()
|
||||
{
|
||||
if(mStopButton == null)
|
||||
{
|
||||
mStopButton = new ToggleButton();
|
||||
mStopButton.setToggleGroup(getPlaybackGroup());
|
||||
mStopButton.setTooltip(new Tooltip("Stop Audio Playback"));
|
||||
mStopButton.setDisable(true);
|
||||
IconNode iconNode = new IconNode(FontAwesome.STOP);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mStopButton.setGraphic(iconNode);
|
||||
}
|
||||
|
||||
return mStopButton;
|
||||
}
|
||||
|
||||
private ToggleButton getAutoRepeatButton()
|
||||
{
|
||||
if(mAutoRepeatButton == null)
|
||||
{
|
||||
mAutoRepeatButton = new ToggleButton();
|
||||
mAutoRepeatButton.setTooltip(new Tooltip("Auto-repeat audio playback"));
|
||||
mAutoRepeatButton.setDisable(true);
|
||||
IconNode iconNode = new IconNode(FontAwesome.REPEAT);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mAutoRepeatButton.setGraphic(iconNode);
|
||||
}
|
||||
|
||||
return mAutoRepeatButton;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
package io.github.dsheirer.audio.call;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
@@ -29,6 +29,7 @@ import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
@@ -36,7 +37,6 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -74,13 +74,13 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
|
||||
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
|
||||
private List<float[]> mAudioBuffers = new CopyOnWriteArrayList();
|
||||
private AtomicInteger mConsumerCount = new AtomicInteger();
|
||||
private AliasList mAliasList;
|
||||
private long mStartTimestamp = System.currentTimeMillis();
|
||||
private long mSampleCount = 0;
|
||||
private boolean mDisposing = false;
|
||||
private AudioSegment mLinkedAudioSegment;
|
||||
private int mTimeslot;
|
||||
private List<String> mLeases = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -94,6 +94,50 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
mIdentifierCollection.setTimeslot(timeslot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a lease to this audio segment. This audio segment will remain alive until all leases are removed.
|
||||
* @param lease to add
|
||||
*/
|
||||
public void addLease(String lease)
|
||||
{
|
||||
synchronized(mLeases)
|
||||
{
|
||||
if(mLeases.contains(lease))
|
||||
{
|
||||
mLog.warn("Attempt to obtain duplicate lease: " + lease);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLeases.add(lease);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the lease from this audio segment. Once all leases are removed, the audio segment is disposed.
|
||||
* @param lease to remove
|
||||
*/
|
||||
public void removeLease(String lease)
|
||||
{
|
||||
synchronized(mLeases)
|
||||
{
|
||||
if(mLeases.contains(lease))
|
||||
{
|
||||
mLeases.remove(lease);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.warn("Attempt to remove a non-existent lease: " + lease);
|
||||
}
|
||||
|
||||
if(mLeases.isEmpty())
|
||||
{
|
||||
mLog.warn("Invoking dispose() on non-leased audio segment - last lease was: " + lease);
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeslot for this audio segment
|
||||
*/
|
||||
@@ -149,28 +193,6 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
return mComplete.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate call audio property. This flag is set to true whenever a duplicate call detection function detects
|
||||
* an audio segment is a duplicate.
|
||||
*/
|
||||
public BooleanProperty duplicateProperty()
|
||||
{
|
||||
return mDuplicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable list of broadcast channels for this audio segment. Broadcast channels are added to this segment
|
||||
* across the life-cycle of the segment. As each new alias identifier is added to the segment, any broadcast
|
||||
* channels assigned to the alias are added to this list. The audio segment producer can also add broadcast
|
||||
* channels to this list.
|
||||
*
|
||||
* @return observable set of broadcast channels
|
||||
*/
|
||||
public ObservableSet<BroadcastChannel> broadcastChannelsProperty()
|
||||
{
|
||||
return mBroadcastChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of broadcast channels from identifier associated aliases for this segment.
|
||||
*/
|
||||
@@ -273,7 +295,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
* Adds the collection of identifiers to this segment's identifier collection
|
||||
* @param identifiers to pre-load into this audio segment
|
||||
*/
|
||||
void addIdentifiers(Collection<Identifier> identifiers)
|
||||
public void addIdentifiers(Collection<Identifier> identifiers)
|
||||
{
|
||||
for(Identifier identifier: identifiers)
|
||||
{
|
||||
@@ -301,24 +323,6 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
return mAudioBuffers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio buffer at the specified index
|
||||
* @param index of the buffer to fetch
|
||||
* @return audio buffer
|
||||
* @throws IllegalArgumentException if requested index is not valid
|
||||
*/
|
||||
public float[] getAudioBuffer(int index)
|
||||
{
|
||||
if(0 <= index && index < getAudioBufferCount())
|
||||
{
|
||||
return mAudioBuffers.get(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("Requested audio buffer at index [" + index + "] does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment has one or more audio buffers
|
||||
*/
|
||||
@@ -339,34 +343,6 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
mLinkedAudioSegment = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the consumer count to indicate that a consumer is currently processing this segment. When the
|
||||
* consumer count returns to zero, this indicates that all consumers are finished with the audio segment and the
|
||||
* resources can be reclaimed.
|
||||
*
|
||||
* Consumer count should only be increased by the producer of the audio segment, or if a consumer distributes the
|
||||
* segment to additional consumers.
|
||||
*/
|
||||
public void incrementConsumerCount()
|
||||
{
|
||||
mConsumerCount.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the consumer count. Consumers of this audio segment should invoke this method to signal that they
|
||||
* will no longer need this audio segment. When all consumers are finished with an audio segment, the audio
|
||||
* segment resources will be reclaimed.
|
||||
*/
|
||||
public void decrementConsumerCount()
|
||||
{
|
||||
int count = mConsumerCount.decrementAndGet();
|
||||
|
||||
if(count <= 0)
|
||||
{
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an audio buffer to this segment. Note the producer of the audio buffer should increment the user count
|
||||
* of the buffer prior to adding it to this segment. This segment will decrement the audio buffer user count once
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio;
|
||||
package io.github.dsheirer.audio.call;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
@@ -41,11 +41,8 @@ public class AudioSegmentBroadcaster<T extends AudioSegment> extends Broadcaster
|
||||
{
|
||||
for(Listener<T> listener : getListeners())
|
||||
{
|
||||
audioSegment.incrementConsumerCount();
|
||||
audioSegment.addLease(listener.getClass().toString());
|
||||
listener.receive(audioSegment);
|
||||
}
|
||||
|
||||
//Decrement consumer counter for this broadcaster
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
}
|
||||
495
src/main/java/io/github/dsheirer/audio/call/Call.java
Normal file
495
src/main/java/io/github/dsheirer/audio/call/Call.java
Normal file
@@ -0,0 +1,495 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.call;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.SequenceGenerator;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.Transient;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* Call database entity.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name="calls", schema="sdrtrunk")
|
||||
public class Call
|
||||
{
|
||||
public static final String COLUMN_CALL_TYPE = "callType";
|
||||
public static final String COLUMN_CHANNEL = "channel";
|
||||
public static final String COLUMN_DUPLICATE = "duplicate";
|
||||
public static final String COLUMN_DURATION = "duration";
|
||||
public static final String COLUMN_EVENT_TIME = "eventTime";
|
||||
public static final String COLUMN_FILE = "file";
|
||||
public static final String COLUMN_FREQUENCY = "frequency";
|
||||
public static final String COLUMN_FROM_ALIAS = "fromAlias";
|
||||
public static final String COLUMN_FROM_ID = "fromId";
|
||||
public static final String COLUMN_ID = "id";
|
||||
public static final String COLUMN_MONITOR = "monitor";
|
||||
public static final String COLUMN_PROTOCOL = "protocol";
|
||||
public static final String COLUMN_RECORD = "record";
|
||||
public static final String COLUMN_STREAM = "stream";
|
||||
public static final String COLUMN_TO_ALIAS = "toAlias";
|
||||
public static final String COLUMN_TO_ID = "toId";
|
||||
public static final String COLUMN_SITE = "site";
|
||||
public static final String COLUMN_SYSTEM = "system";
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "id_generator")
|
||||
@SequenceGenerator(name = "id_generator", allocationSize = 1)
|
||||
@Column(name=COLUMN_ID)
|
||||
private long id;
|
||||
@Column(name=COLUMN_CALL_TYPE)
|
||||
private String callType;
|
||||
@Column(name=COLUMN_CHANNEL)
|
||||
private String channel;
|
||||
@Column(name=COLUMN_DUPLICATE)
|
||||
private boolean duplicate;
|
||||
@Column(name=COLUMN_DURATION)
|
||||
private double duration;
|
||||
@Column(name=COLUMN_EVENT_TIME)
|
||||
private long eventTime;
|
||||
@Column(name=COLUMN_FILE)
|
||||
private String file;
|
||||
@Column(name=COLUMN_FREQUENCY)
|
||||
private double frequency;
|
||||
@Column(name=COLUMN_FROM_ALIAS)
|
||||
private String fromAlias;
|
||||
@Column(name=COLUMN_FROM_ID)
|
||||
private String fromId;
|
||||
@Column(name=COLUMN_MONITOR)
|
||||
private int monitor;
|
||||
@Column(name=COLUMN_PROTOCOL)
|
||||
private String protocol;
|
||||
@Column(name=COLUMN_RECORD)
|
||||
private boolean record;
|
||||
@Column(name=COLUMN_SITE)
|
||||
private String site;
|
||||
@Column(name=COLUMN_STREAM)
|
||||
private boolean stream;
|
||||
@Column(name=COLUMN_SYSTEM)
|
||||
private String system;
|
||||
@Column(name=COLUMN_TO_ALIAS)
|
||||
private String toAlias;
|
||||
@Column(name=COLUMN_TO_ID)
|
||||
private String toId;
|
||||
|
||||
@Transient
|
||||
private boolean mComplete = true;
|
||||
@Transient
|
||||
private LongProperty mLastUpdated = new SimpleLongProperty();
|
||||
@Transient
|
||||
private StringProperty mPlaybackChannel = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public Call()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Record ID, auto-assigned by the database.
|
||||
* @return id
|
||||
*/
|
||||
public long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp for the event
|
||||
* @return timestamp in milliseconds since epoch
|
||||
*/
|
||||
public long getEventTime()
|
||||
{
|
||||
return eventTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timestamp
|
||||
* @param eventTime in ms.
|
||||
*/
|
||||
public void setEventTime(long eventTime)
|
||||
{
|
||||
this.eventTime = eventTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* From identifier
|
||||
* @return from ID
|
||||
*/
|
||||
public String getFromId()
|
||||
{
|
||||
return fromId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the from identifier
|
||||
* @param fromId ID
|
||||
*/
|
||||
public void setFromId(String fromId)
|
||||
{
|
||||
this.fromId = fromId;
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* From alias
|
||||
* @return from alias
|
||||
*/
|
||||
public String getFromAlias()
|
||||
{
|
||||
return fromAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the from alias value.
|
||||
* @param fromAlias value
|
||||
*/
|
||||
public void setFromAlias(String fromAlias)
|
||||
{
|
||||
this.fromAlias = fromAlias;
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* To identifier
|
||||
* @return to
|
||||
*/
|
||||
public String getToId()
|
||||
{
|
||||
return toId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the to identifier
|
||||
* @param toId value
|
||||
*/
|
||||
public void setToId(String toId)
|
||||
{
|
||||
this.toId = toId;
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* To alias value
|
||||
* @return alias value
|
||||
*/
|
||||
public String getToAlias()
|
||||
{
|
||||
return toAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the to alias value.
|
||||
* @param toAlias value
|
||||
*/
|
||||
public void setToAlias(String toAlias)
|
||||
{
|
||||
this.toAlias = toAlias;
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* System name
|
||||
* @return name
|
||||
*/
|
||||
public String getSystem()
|
||||
{
|
||||
return system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system name
|
||||
* @param system name
|
||||
*/
|
||||
public void setSystem(String system)
|
||||
{
|
||||
this.system = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Site name
|
||||
* @return site
|
||||
*/
|
||||
public String getSite()
|
||||
{
|
||||
return site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the site name
|
||||
* @param site name
|
||||
*/
|
||||
public void setSite(String site)
|
||||
{
|
||||
this.site = site;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel name
|
||||
* @return channel
|
||||
*/
|
||||
public String getChannel()
|
||||
{
|
||||
return channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the channel name
|
||||
* @param channel name
|
||||
*/
|
||||
public void setChannel(String channel)
|
||||
{
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frequency for the channel
|
||||
* @return frequency.
|
||||
*/
|
||||
public double getFrequency()
|
||||
{
|
||||
return frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the frequency value for the channel.
|
||||
* @param frequency value.
|
||||
*/
|
||||
public void setFrequency(double frequency)
|
||||
{
|
||||
this.frequency = frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* File path for the call
|
||||
* @return file path
|
||||
*/
|
||||
public String getFile()
|
||||
{
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file path for the call.
|
||||
* @param file path
|
||||
*/
|
||||
public void setFile(String file)
|
||||
{
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration of the call in seconds
|
||||
* @return duration seconds
|
||||
*/
|
||||
public double getDuration()
|
||||
{
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the call duration.
|
||||
* @param duration value in seconds
|
||||
*/
|
||||
public void setDuration(double duration)
|
||||
{
|
||||
this.duration = duration;
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Radio protocol
|
||||
* @return protocol
|
||||
*/
|
||||
public String getProtocol()
|
||||
{
|
||||
return protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the radio protocol
|
||||
* @param protocol for the call
|
||||
*/
|
||||
public void setProtocol(String protocol)
|
||||
{
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this call is complete and will receive no further updates.
|
||||
* @return true if complete.
|
||||
*/
|
||||
public boolean isComplete()
|
||||
{
|
||||
return mComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the complete status for this call.
|
||||
* @param complete true if complete
|
||||
*/
|
||||
public void setComplete(boolean complete)
|
||||
{
|
||||
mComplete = complete;
|
||||
updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call type
|
||||
* @return call type
|
||||
*/
|
||||
public String getCallType()
|
||||
{
|
||||
return callType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the call type
|
||||
* @param callType of call
|
||||
*/
|
||||
public void setCallType(String callType)
|
||||
{
|
||||
this.callType = callType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this call was flagged as a duplicate
|
||||
* @return true if duplicate
|
||||
*/
|
||||
public boolean isDuplicate()
|
||||
{
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duplicate flag for this call.
|
||||
* @param duplicate flag
|
||||
*/
|
||||
public void setDuplicate(boolean duplicate)
|
||||
{
|
||||
this.duplicate = duplicate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor/listen priority
|
||||
* @return priority where a -1 value is do not monitor.
|
||||
*/
|
||||
public int getMonitor()
|
||||
{
|
||||
return monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the monitor/listen priority
|
||||
* @param monitor priority
|
||||
*/
|
||||
public void setMonitor(int monitor)
|
||||
{
|
||||
this.monitor = monitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the call is recordable/retainable.
|
||||
* @return is recordable
|
||||
*/
|
||||
public boolean isRecord()
|
||||
{
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the record/retain flag.
|
||||
* @param record flag
|
||||
*/
|
||||
public void setRecord(boolean record)
|
||||
{
|
||||
this.record = record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the call is streamable.
|
||||
* @return streamable
|
||||
*/
|
||||
public boolean isStream()
|
||||
{
|
||||
return stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stream flag
|
||||
* @param stream flag
|
||||
*/
|
||||
public void setStream(boolean stream)
|
||||
{
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last updated timestamp.
|
||||
*/
|
||||
private void updateTimestamp()
|
||||
{
|
||||
if(Platform.isFxApplicationThread())
|
||||
{
|
||||
lastUpdatedProperty().set(System.currentTimeMillis());
|
||||
}
|
||||
else
|
||||
{
|
||||
Platform.runLater(() -> lastUpdatedProperty().set(System.currentTimeMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Last updated timestamp. This value is updated each time any attribute in the call record is updated so that
|
||||
* consumer processes can monitor this property to be notified of updates.
|
||||
* @return timestamp of last update in milliseconds.
|
||||
*/
|
||||
public LongProperty lastUpdatedProperty()
|
||||
{
|
||||
return mLastUpdated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current playback channel for this call.
|
||||
* @return current playback channel or null if this call is not being played back.
|
||||
*/
|
||||
public StringProperty playbackChannelProperty()
|
||||
{
|
||||
return mPlaybackChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj)
|
||||
{
|
||||
return obj instanceof Call c && getId() == c.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.call;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
|
||||
/**
|
||||
* Calls database repository
|
||||
*/
|
||||
public interface CallRepository extends JpaRepository<Call,Long>
|
||||
{
|
||||
@Query("SELECT t FROM Call t WHERE t.toId like ?1 OR t.toAlias like ?1 OR t.fromId like ?1 OR t.fromAlias like ?1")
|
||||
Page<Call> findByAnyIdWithPagination(String value, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Call t WHERE t.toId like ?1 OR t.toAlias like ?1")
|
||||
Page<Call> findByToWithPagination(String value, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Call t WHERE t.fromId like ?1 OR t.fromAlias like ?1")
|
||||
Page<Call> findByFromWithPagination(String value, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Call t WHERE t.callType like ?1")
|
||||
Page<Call> findByCallTypeWithPagination(String value, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Call t WHERE t.channel like ?1")
|
||||
Page<Call> findByChannelWithPagination(String value, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Call t WHERE t.protocol like ?1")
|
||||
Page<Call> findByProtocolWithPagination(String value, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Call t WHERE t.site like ?1")
|
||||
Page<Call> findBySiteWithPagination(String value, Pageable pageable);
|
||||
|
||||
@Query("SELECT t FROM Call t WHERE t.system like ?1")
|
||||
Page<Call> findBySystemWithPagination(String value, Pageable pageable);
|
||||
|
||||
/**
|
||||
* Find all calls between the timestamp range arguments.
|
||||
* @param start timestamp
|
||||
* @param end timestamp
|
||||
* @return pageable set of calls.
|
||||
*/
|
||||
Page<Call> findCallsByEventTimeBetweenOrderByEventTime(long start, long end, Pageable pageable);
|
||||
}
|
||||
971
src/main/java/io/github/dsheirer/audio/call/CallView.java
Normal file
971
src/main/java/io/github/dsheirer/audio/call/CallView.java
Normal file
@@ -0,0 +1,971 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.call;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.audio.AudioManager;
|
||||
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelController;
|
||||
import io.github.dsheirer.audio.playbackfx.AudioPlaybackController;
|
||||
import io.github.dsheirer.audio.playbackfx.PlaybackMode;
|
||||
import io.github.dsheirer.gui.control.TimestampTableCellFactory;
|
||||
import io.github.dsheirer.gui.javafx.table.TableColumnEditor;
|
||||
import io.github.dsheirer.gui.javafx.table.TableViewColumnController;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.SplitPane;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Callback;
|
||||
import jiconfont.icons.elusive.Elusive;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.javafx.IconNode;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
|
||||
/**
|
||||
* Audio call events playback and audio event database control/view.
|
||||
*/
|
||||
public class CallView extends VBox implements IPageRequestListener
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CallView.class);
|
||||
private static final Sort DEFAULT_SORT = Sort.by("eventTime").descending();
|
||||
|
||||
@Resource
|
||||
private AudioManager mAudioManager;
|
||||
@Resource
|
||||
private CallRepository mCallRepository;
|
||||
@Resource
|
||||
private AudioPlaybackController mAudioPlaybackController;
|
||||
|
||||
private CallEventListener mCallEventListener = new CallEventListener();
|
||||
private SortOrderListener mSortOrderListener = new SortOrderListener();
|
||||
private ObservableList<Call> mCalls = FXCollections.observableArrayList();
|
||||
private TableView<Call> mCallTableView;
|
||||
private TableViewColumnController mCallTableViewMonitor;
|
||||
private ProgressIndicator mPlaceholderProgressIndicator;
|
||||
private HBox mSearchBox;
|
||||
private HBox mPagingBox;
|
||||
private TextField mSearchField;
|
||||
private PagingController mPagingController;
|
||||
private ComboBox<Integer> mPageSizeComboBox;
|
||||
private Label mCallsCountLabel;
|
||||
private LongProperty mCallCount = new SimpleLongProperty();
|
||||
private Sort mSort = DEFAULT_SORT;
|
||||
private ComboBox<SearchColumn> mSearchColumnComboBox;
|
||||
private CheckBox mSearchExactCheckBox;
|
||||
private AudioPlaybackControlView mAudioPlaybackControlView;
|
||||
private Button mAutoModeLeftButton;
|
||||
private Button mAutoModeRightButton;
|
||||
private Button mLockModeLeftButton;
|
||||
private Button mLockModeRightButton;
|
||||
private TableColumnEditor mTableColumnEditor;
|
||||
private SplitPane mTableSplitPane;
|
||||
private Button mConfigureColumnsButton;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public CallView()
|
||||
{
|
||||
mCallCount.addListener((observable, oldValue, newValue) -> {
|
||||
getCallsCountLabel().setText("Calls: " + newValue);
|
||||
int pageCount = (int)(mCallCount.get() / getPageSize());
|
||||
if(mCallCount.get() % getPageSize() != 0)
|
||||
{
|
||||
pageCount++;
|
||||
}
|
||||
getPagingController().update(getPagingController().getCurrentPage(), pageCount);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Post instantiation/startup steps.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
//Register the audio playback control view as a playback status listener with the primary audio channel controller
|
||||
mAudioPlaybackController.getPrimaryController().add(getAudioPlaybackControlView());
|
||||
|
||||
HBox controlBox = new HBox();
|
||||
controlBox.setAlignment(Pos.CENTER);
|
||||
controlBox.setSpacing(2);
|
||||
controlBox.setPadding(new Insets(5, 5, 5, 5));
|
||||
getAudioPlaybackControlView().setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(getSearchBox(), Priority.ALWAYS);
|
||||
getSearchBox().setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
Separator separator1 = new Separator(Orientation.VERTICAL);
|
||||
separator1.setPadding(new Insets(0, 10, 0, 10));
|
||||
Separator separator2 = new Separator(Orientation.VERTICAL);
|
||||
separator2.setPadding(new Insets(0, 10, 0, 10));
|
||||
Label autoLabel = new Label("Auto:");
|
||||
Label lockLabel = new Label("Lock:");
|
||||
getSearchExactCheckBox().setPadding(new Insets(0, 10, 0, 0));
|
||||
controlBox.getChildren().addAll(getAudioPlaybackControlView(), separator1, autoLabel, getAutoModeLeftButton(),
|
||||
getAutoModeRightButton(), separator2, lockLabel, getLockModeLeftButton(), getLockModeRightButton(),
|
||||
getSearchBox(), getConfigureColumnsButton());
|
||||
|
||||
getTableColumnEditor().closedProperty().addListener((observable, previouslyClosed, closed) -> {
|
||||
if(closed && !previouslyClosed)
|
||||
{
|
||||
getTableSplitPane().getItems().remove(getTableColumnEditor());
|
||||
}
|
||||
});
|
||||
|
||||
VBox.setVgrow(getTableSplitPane(), Priority.ALWAYS);
|
||||
getChildren().addAll(controlBox, getTableSplitPane(), getPagingBox());
|
||||
|
||||
getPagingController().setPadding(new Insets(5, 5, 5, 5));
|
||||
getPagingController().update(1, 20);
|
||||
|
||||
//Register for call event notifications
|
||||
mAudioManager.add(mCallEventListener);
|
||||
|
||||
getPlaceholderProgressIndicator().setVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-destruction or shutdown steps
|
||||
*/
|
||||
@PreDestroy
|
||||
public void preDestroy()
|
||||
{
|
||||
//De-register from call event notifications
|
||||
mAudioManager.remove(mCallEventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows first page of results.
|
||||
*/
|
||||
private void showFirstPage()
|
||||
{
|
||||
showPage(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the page to view. This implements the IPageRequestListener interface.
|
||||
* @param page number to display.
|
||||
*/
|
||||
@Override
|
||||
public void showPage(int page)
|
||||
{
|
||||
if(page >= 1)
|
||||
{
|
||||
Platform.runLater(() -> getPlaceholderProgressIndicator().setVisible(true));
|
||||
|
||||
final String searchTerm = getSearchField().getText();
|
||||
final SearchColumn searchColumn = getSearchColumnComboBox().getSelectionModel().getSelectedItem();
|
||||
final boolean wildcard = !getSearchExactCheckBox().isSelected();
|
||||
|
||||
//Spin the query off onto a cached thread and then place the results back onto the FX thread.
|
||||
ThreadPool.CACHED.submit(() ->
|
||||
{
|
||||
final PageRequest pageRequest = PageRequest.of(page - 1, getPageSize(), mSort);
|
||||
|
||||
try
|
||||
{
|
||||
Page<Call> callPage = null;
|
||||
|
||||
if(searchTerm != null && !searchTerm.isEmpty() && searchColumn != null)
|
||||
{
|
||||
final String wildcardSearch = (wildcard ? "%" : "") + searchTerm + (wildcard ? "%" : "");
|
||||
|
||||
switch(searchColumn)
|
||||
{
|
||||
case ANY_ID -> callPage = mCallRepository.findByAnyIdWithPagination(wildcardSearch, pageRequest);
|
||||
case CALL_TYPE -> callPage = mCallRepository.findByCallTypeWithPagination(wildcardSearch, pageRequest);
|
||||
case CHANNEL -> callPage = mCallRepository.findByChannelWithPagination(wildcardSearch, pageRequest);
|
||||
case FROM -> callPage = mCallRepository.findByFromWithPagination(wildcardSearch, pageRequest);
|
||||
case PROTOCOL -> callPage = mCallRepository.findByProtocolWithPagination(wildcardSearch, pageRequest);
|
||||
case SITE -> callPage = mCallRepository.findBySiteWithPagination(wildcardSearch, pageRequest);
|
||||
case SYSTEM -> callPage = mCallRepository.findBySystemWithPagination(wildcardSearch, pageRequest);
|
||||
case TO -> callPage = mCallRepository.findByToWithPagination(wildcardSearch, pageRequest);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callPage = mCallRepository.findAll(pageRequest);
|
||||
}
|
||||
|
||||
if(callPage != null)
|
||||
{
|
||||
final Page<Call> finalCallPage = callPage;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
mCalls.clear();
|
||||
mCalls.addAll(finalCallPage.stream().toList());
|
||||
getPlaceholderProgressIndicator().setVisible(false);
|
||||
getPagingController().update(finalCallPage.getNumber() + 1, finalCallPage.getTotalPages());
|
||||
mCallCount.set(finalCallPage.getTotalElements());
|
||||
});
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
LOGGER.error("Error submitting paging [" + page + "] query to database", t);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getPlaceholderProgressIndicator().setVisible(false);
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
if(getParent() != null)
|
||||
{
|
||||
alert.initOwner(getParent().getScene().getWindow());
|
||||
}
|
||||
alert.setHeaderText("Database Error!");
|
||||
alert.setContentText("Error retrieving page " + page + " from the database - reason: " + t.getMessage());
|
||||
alert.showAndWait();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGER.warn("Invalid page [" + page + "] number request");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sort order for the table and resets the page to page 1.
|
||||
*/
|
||||
private void updateSortOrder()
|
||||
{
|
||||
mSort = null;
|
||||
|
||||
for(TableColumn<Call,?> tableColumn: getCallTableView().getSortOrder())
|
||||
{
|
||||
Sort columnSort = Sort.by(tableColumn.getId());
|
||||
if(tableColumn.getSortType() == TableColumn.SortType.DESCENDING)
|
||||
{
|
||||
columnSort = columnSort.descending();
|
||||
}
|
||||
|
||||
if(mSort == null)
|
||||
{
|
||||
mSort = columnSort;
|
||||
}
|
||||
else
|
||||
{
|
||||
mSort = mSort.and(columnSort);
|
||||
}
|
||||
}
|
||||
|
||||
if(mSort == null)
|
||||
{
|
||||
mSort = DEFAULT_SORT;
|
||||
}
|
||||
|
||||
showFirstPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current page size
|
||||
*/
|
||||
private int getPageSize()
|
||||
{
|
||||
return getPageSizeComboBox().getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure column order and visibility button
|
||||
*/
|
||||
private Button getConfigureColumnsButton()
|
||||
{
|
||||
if(mConfigureColumnsButton == null)
|
||||
{
|
||||
mConfigureColumnsButton = new Button();
|
||||
mConfigureColumnsButton.setTooltip(new Tooltip("Configure call table column ordering and visibility"));
|
||||
IconNode icon = new IconNode(FontAwesome.COG);
|
||||
mConfigureColumnsButton.setGraphic(icon);
|
||||
mConfigureColumnsButton.setOnAction(event -> {
|
||||
//Set the split pane to expose the column configuration panel.
|
||||
if(!getTableSplitPane().getItems().contains(getTableColumnEditor()))
|
||||
{
|
||||
getTableSplitPane().getItems().add(getTableColumnEditor());
|
||||
}
|
||||
getTableColumnEditor().closedProperty().set(false);
|
||||
double splitWidth = getTableSplitPane().getWidth();
|
||||
double scale = 0.5;
|
||||
if(splitWidth > 260)
|
||||
{
|
||||
scale = (splitWidth - 260) / splitWidth;
|
||||
}
|
||||
getTableSplitPane().setDividerPosition(0, scale);
|
||||
});
|
||||
}
|
||||
|
||||
return mConfigureColumnsButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split pane for call table and table column editor
|
||||
*/
|
||||
private SplitPane getTableSplitPane()
|
||||
{
|
||||
if(mTableSplitPane == null)
|
||||
{
|
||||
mTableSplitPane = new SplitPane(getCallTableView());
|
||||
}
|
||||
|
||||
return mTableSplitPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Table column order/visibility editor
|
||||
*/
|
||||
private TableColumnEditor getTableColumnEditor()
|
||||
{
|
||||
if(mTableColumnEditor == null)
|
||||
{
|
||||
mTableColumnEditor = new TableColumnEditor(getCallTableView(), mCallTableViewMonitor);
|
||||
}
|
||||
|
||||
return mTableColumnEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback mode to auto
|
||||
*/
|
||||
private Button getAutoModeLeftButton()
|
||||
{
|
||||
if(mAutoModeLeftButton == null)
|
||||
{
|
||||
mAutoModeLeftButton = new Button("Left");
|
||||
mAutoModeLeftButton.setTooltip(new Tooltip("Set left audio channel to auto playback mode"));
|
||||
AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
|
||||
mAutoModeLeftButton.disableProperty().bind(left.playbackModeProperty().isEqualTo(PlaybackMode.AUTO));
|
||||
mAutoModeLeftButton.setOnAction(event -> {
|
||||
final AudioPlaybackChannelController controller = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
|
||||
if(controller != null)
|
||||
{
|
||||
getCallTableView().getSelectionModel().clearSelection();
|
||||
controller.auto();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mAutoModeLeftButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback mode to auto
|
||||
*/
|
||||
private Button getAutoModeRightButton()
|
||||
{
|
||||
if(mAutoModeRightButton == null)
|
||||
{
|
||||
mAutoModeRightButton = new Button("Right");
|
||||
mAutoModeRightButton.setTooltip(new Tooltip("Set right audio channel to auto playback mode"));
|
||||
AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
|
||||
mAutoModeRightButton.disableProperty().bind(right.playbackModeProperty().isEqualTo(PlaybackMode.AUTO));
|
||||
mAutoModeRightButton.setOnAction(event -> {
|
||||
final AudioPlaybackChannelController controller = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
|
||||
if(controller != null)
|
||||
{
|
||||
getCallTableView().getSelectionModel().clearSelection();
|
||||
controller.auto();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mAutoModeRightButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback mode to auto - locked to the currently selected TO identifier to the left channel
|
||||
*/
|
||||
private Button getLockModeLeftButton()
|
||||
{
|
||||
if(mLockModeLeftButton == null)
|
||||
{
|
||||
mLockModeLeftButton = new Button("Left");
|
||||
mLockModeLeftButton.setTooltip(new Tooltip("Lock the left audio channel for auto playback of selected call talkgroup"));
|
||||
mLockModeLeftButton.setDisable(true);
|
||||
mLockModeLeftButton.setOnAction(event -> {
|
||||
final AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
|
||||
if(left != null)
|
||||
{
|
||||
final Call selected = getCallTableView().getSelectionModel().getSelectedItem();
|
||||
getCallTableView().getSelectionModel().clearSelection();
|
||||
getLockModeLeftButton().setDisable(true);
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
//If the right controller is locked for this same call, reset it to auto so that only 1 is locked
|
||||
final AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
|
||||
if(right.isLockedFor(selected))
|
||||
{
|
||||
right.auto();
|
||||
}
|
||||
|
||||
left.lockAutoTo(selected.getToId(), selected.getSystem());
|
||||
}
|
||||
else
|
||||
{
|
||||
left.auto();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mLockModeLeftButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback mode to auto - locked to the currently selected TO identifier to the left channel
|
||||
*/
|
||||
private Button getLockModeRightButton()
|
||||
{
|
||||
if(mLockModeRightButton == null)
|
||||
{
|
||||
mLockModeRightButton = new Button("Right");
|
||||
mLockModeRightButton.setTooltip(new Tooltip("Lock the left audio channel for auto playback of selected call talkgroup"));
|
||||
mLockModeRightButton.setDisable(true);
|
||||
mLockModeRightButton.setOnAction(event -> {
|
||||
final AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
|
||||
if(right != null)
|
||||
{
|
||||
final Call selected = getCallTableView().getSelectionModel().getSelectedItem();
|
||||
getCallTableView().getSelectionModel().clearSelection();
|
||||
getLockModeRightButton().setDisable(true);
|
||||
|
||||
if(selected != null)
|
||||
{
|
||||
//If the left controller is locked for this same call, reset it to auto so that only 1 is locked
|
||||
final AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
|
||||
if(left.isLockedFor(selected))
|
||||
{
|
||||
left.auto();
|
||||
}
|
||||
|
||||
right.lockAutoTo(selected.getToId(), selected.getSystem());
|
||||
}
|
||||
else
|
||||
{
|
||||
right.auto();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mLockModeRightButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio playback control view.
|
||||
*/
|
||||
private AudioPlaybackControlView getAudioPlaybackControlView()
|
||||
{
|
||||
if(mAudioPlaybackControlView == null)
|
||||
{
|
||||
mAudioPlaybackControlView = new AudioPlaybackControlView();
|
||||
}
|
||||
|
||||
return mAudioPlaybackControlView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls label to display the number of calls in the database.
|
||||
*/
|
||||
private Label getCallsCountLabel()
|
||||
{
|
||||
if(mCallsCountLabel == null)
|
||||
{
|
||||
mCallsCountLabel = new Label("Calls: 0");
|
||||
}
|
||||
|
||||
return mCallsCountLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles wildcard searching
|
||||
* @return true for exact match or false for wildcard/fuzzy matching.
|
||||
*/
|
||||
private CheckBox getSearchExactCheckBox()
|
||||
{
|
||||
if(mSearchExactCheckBox == null)
|
||||
{
|
||||
mSearchExactCheckBox = new CheckBox("Exact Match");
|
||||
mSearchExactCheckBox.setTooltip(new Tooltip("Toggle exact matching (checked) or wildcard/fuzzy " +
|
||||
"matching (unchecked)"));
|
||||
mSearchExactCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> showFirstPage());
|
||||
}
|
||||
|
||||
return mSearchExactCheckBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page size selection combo box.
|
||||
*
|
||||
* Note: when the user changes the page size, we always request page 1 again.
|
||||
*/
|
||||
private ComboBox<Integer> getPageSizeComboBox()
|
||||
{
|
||||
if(mPageSizeComboBox == null)
|
||||
{
|
||||
ObservableList<Integer> pageSizes = FXCollections.observableArrayList();
|
||||
pageSizes.addAll(25, 50, 100, 200);
|
||||
mPageSizeComboBox = new ComboBox<>(pageSizes);
|
||||
mPageSizeComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> showFirstPage());
|
||||
mPageSizeComboBox.setTooltip(new Tooltip("Select the page size"));
|
||||
mPageSizeComboBox.getSelectionModel().select(0);
|
||||
}
|
||||
|
||||
return mPageSizeComboBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page size selection combo box.
|
||||
*
|
||||
* Note: when the user changes the page size, we always request page 1 again.
|
||||
*/
|
||||
private ComboBox<SearchColumn> getSearchColumnComboBox()
|
||||
{
|
||||
if(mSearchColumnComboBox == null)
|
||||
{
|
||||
mSearchColumnComboBox = new ComboBox<>(FXCollections.observableArrayList(SearchColumn.values()));
|
||||
mSearchColumnComboBox.getSelectionModel().select(SearchColumn.ANY_ID);
|
||||
mSearchColumnComboBox.setTooltip(new Tooltip("Select the field(s) to search against"));
|
||||
}
|
||||
|
||||
return mSearchColumnComboBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paging controls panel
|
||||
*/
|
||||
private HBox getPagingBox()
|
||||
{
|
||||
if(mPagingBox == null)
|
||||
{
|
||||
mPagingBox = new HBox();
|
||||
mPagingBox.setAlignment(Pos.CENTER_LEFT);
|
||||
mPagingBox.setSpacing(3);
|
||||
Label pageSizeLabel = new Label("Page Size:");
|
||||
pageSizeLabel.setPadding(new Insets(0, 2, 0, 10));
|
||||
getCallsCountLabel().setPadding(new Insets(0, 0, 0, 10));
|
||||
mPagingBox.getChildren().addAll(getPagingController(), pageSizeLabel, getPageSizeComboBox(), getCallsCountLabel());
|
||||
}
|
||||
|
||||
return mPagingBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call table view
|
||||
* @return
|
||||
*/
|
||||
private TableView<Call> getCallTableView()
|
||||
{
|
||||
if(mCallTableView == null)
|
||||
{
|
||||
mCallTableView = new TableView<>(mCalls);
|
||||
mCallTableView.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
TableColumn playbackColumn = new TableColumn();
|
||||
playbackColumn.setSortable(false);
|
||||
playbackColumn.setId("playback");
|
||||
playbackColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
IconNode iconNode = new IconNode(Elusive.VOLUME_UP);
|
||||
iconNode.setIconSize(16);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
playbackColumn.setGraphic(iconNode);
|
||||
playbackColumn.setCellValueFactory(new PropertyValueFactory<>("playbackChannel"));
|
||||
playbackColumn.setPrefWidth(40);
|
||||
|
||||
TableColumn eventTimeColumn = new TableColumn();
|
||||
eventTimeColumn.setId("eventTime");
|
||||
eventTimeColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
eventTimeColumn.setText("Date/Time");
|
||||
eventTimeColumn.setCellValueFactory(new PropertyValueFactory<Call, Long>(Call.COLUMN_EVENT_TIME));
|
||||
eventTimeColumn.setCellFactory(new TimestampTableCellFactory<Call>());
|
||||
eventTimeColumn.setPrefWidth(160);
|
||||
|
||||
TableColumn durationColumn = new TableColumn();
|
||||
durationColumn.setId("duration");
|
||||
durationColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
durationColumn.setText("Duration");
|
||||
durationColumn.setCellValueFactory(new PropertyValueFactory<Call, Double>(Call.COLUMN_DURATION));
|
||||
durationColumn.setPrefWidth(70);
|
||||
|
||||
TableColumn callTypeColumn = new TableColumn();
|
||||
callTypeColumn.setId("callType");
|
||||
callTypeColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
callTypeColumn.setText("Type");
|
||||
callTypeColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_CALL_TYPE));
|
||||
callTypeColumn.setPrefWidth(85);
|
||||
|
||||
TableColumn toIdColumn = new TableColumn();
|
||||
toIdColumn.setId("toId");
|
||||
toIdColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
toIdColumn.setText("To");
|
||||
toIdColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_TO_ID));
|
||||
toIdColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn toAliasColumn = new TableColumn();
|
||||
toAliasColumn.setId("toAlias");
|
||||
toAliasColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
toAliasColumn.setText("To Alias");
|
||||
toAliasColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_TO_ALIAS));
|
||||
toAliasColumn.setPrefWidth(100);
|
||||
|
||||
TableColumn fromIdColumn = new TableColumn();
|
||||
fromIdColumn.setId("fromId");
|
||||
fromIdColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
fromIdColumn.setText("From");
|
||||
fromIdColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_FROM_ID));
|
||||
fromIdColumn.setPrefWidth(70);
|
||||
|
||||
TableColumn fromAliasColumn = new TableColumn();
|
||||
fromAliasColumn.setId("fromAlias");
|
||||
fromAliasColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
fromAliasColumn.setText("From Alias");
|
||||
fromAliasColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_FROM_ALIAS));
|
||||
fromAliasColumn.setPrefWidth(100);
|
||||
|
||||
TableColumn protocolColumn = new TableColumn();
|
||||
protocolColumn.setId("protocol");
|
||||
protocolColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
protocolColumn.setText("Protocol");
|
||||
protocolColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_PROTOCOL));
|
||||
protocolColumn.setPrefWidth(90);
|
||||
|
||||
TableColumn frequencyColumn = new TableColumn();
|
||||
frequencyColumn.setId("frequency");
|
||||
frequencyColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
frequencyColumn.setText("Frequency");
|
||||
frequencyColumn.setCellValueFactory(new PropertyValueFactory<Call, Double>(Call.COLUMN_FREQUENCY));
|
||||
frequencyColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn systemColumn = new TableColumn();
|
||||
systemColumn.setId("system");
|
||||
systemColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
systemColumn.setText("System");
|
||||
systemColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_SYSTEM));
|
||||
systemColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn siteColumn = new TableColumn();
|
||||
siteColumn.setVisible(false);
|
||||
siteColumn.setId("mSite");
|
||||
siteColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
siteColumn.setText("Site");
|
||||
siteColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_SITE));
|
||||
siteColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn channelColumn = new TableColumn();
|
||||
channelColumn.setVisible(false);
|
||||
channelColumn.setId("channel");
|
||||
channelColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
channelColumn.setText("Channel");
|
||||
channelColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_CHANNEL));
|
||||
channelColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn duplicateColumn = new TableColumn();
|
||||
duplicateColumn.setVisible(false);
|
||||
duplicateColumn.setId("duplicate");
|
||||
duplicateColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
duplicateColumn.setText("Duplicate");
|
||||
duplicateColumn.setCellValueFactory(new PropertyValueFactory<Call, Boolean>(Call.COLUMN_DUPLICATE));
|
||||
duplicateColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn recordColumn = new TableColumn();
|
||||
recordColumn.setVisible(false);
|
||||
recordColumn.setId("record");
|
||||
recordColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
recordColumn.setText("Record");
|
||||
recordColumn.setCellValueFactory(new PropertyValueFactory<Call, Boolean>(Call.COLUMN_RECORD));
|
||||
recordColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn streamColumn = new TableColumn();
|
||||
streamColumn.setVisible(false);
|
||||
streamColumn.setId("stream");
|
||||
streamColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
streamColumn.setText("Stream");
|
||||
streamColumn.setCellValueFactory(new PropertyValueFactory<Call, Boolean>(Call.COLUMN_STREAM));
|
||||
streamColumn.setPrefWidth(80);
|
||||
|
||||
TableColumn monitorColumn = new TableColumn();
|
||||
monitorColumn.setVisible(false);
|
||||
monitorColumn.setId("monitor");
|
||||
monitorColumn.sortTypeProperty().addListener(mSortOrderListener);
|
||||
monitorColumn.setText("Monitor");
|
||||
monitorColumn.setCellValueFactory(new PropertyValueFactory<Call, Integer>(Call.COLUMN_MONITOR));
|
||||
monitorColumn.setCellFactory(new PriorityCellFactory());
|
||||
monitorColumn.setPrefWidth(80);
|
||||
|
||||
mCallTableView.getSortOrder().addListener(mSortOrderListener);
|
||||
mCallTableView.getColumns().addAll(playbackColumn, eventTimeColumn, durationColumn, callTypeColumn,
|
||||
toIdColumn, toAliasColumn, fromIdColumn, fromAliasColumn, protocolColumn, frequencyColumn,
|
||||
systemColumn, siteColumn, channelColumn, duplicateColumn, recordColumn, streamColumn, monitorColumn);
|
||||
mCallTableView.setPlaceholder(getPlaceholderProgressIndicator());
|
||||
mCallTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, selectedCall) -> {
|
||||
if(selectedCall != null)
|
||||
{
|
||||
mAudioPlaybackController.replay(MixerChannel.LEFT, selectedCall);
|
||||
}
|
||||
else if(oldValue != null && selectedCall == null)
|
||||
{
|
||||
//We were in replay but now we don't have a selected call
|
||||
mAudioPlaybackController.auto(MixerChannel.LEFT, null, null);
|
||||
}
|
||||
|
||||
AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
|
||||
AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
|
||||
getLockModeLeftButton().setDisable(selectedCall == null || left.isLockedFor(selectedCall));
|
||||
getLockModeRightButton().setDisable(selectedCall == null || right.isLockedFor(selectedCall));
|
||||
});
|
||||
getPlaceholderProgressIndicator().setVisible(true);
|
||||
|
||||
//Bind the playback control view to the primary audio channel playback controller
|
||||
getAudioPlaybackControlView().mediaPlayerProperty()
|
||||
.bind(mAudioPlaybackController.getPrimaryController().mediaPlayerProperty());
|
||||
|
||||
mCallTableViewMonitor = new TableViewColumnController(mCallTableView, "call_table_view");
|
||||
}
|
||||
|
||||
return mCallTableView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paging controller
|
||||
*/
|
||||
public PagingController getPagingController()
|
||||
{
|
||||
if(mPagingController == null)
|
||||
{
|
||||
mPagingController = new PagingController(this::showPage);
|
||||
}
|
||||
|
||||
return mPagingController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress indicator to convey data loading status when the table content is being updated.
|
||||
* @return progress indicator
|
||||
*/
|
||||
public ProgressIndicator getPlaceholderProgressIndicator()
|
||||
{
|
||||
if(mPlaceholderProgressIndicator == null)
|
||||
{
|
||||
mPlaceholderProgressIndicator = new ProgressIndicator();
|
||||
mPlaceholderProgressIndicator.setVisible(false);
|
||||
mPlaceholderProgressIndicator.setProgress(-1); //always spinning
|
||||
}
|
||||
|
||||
return mPlaceholderProgressIndicator;
|
||||
}
|
||||
|
||||
private HBox getSearchBox()
|
||||
{
|
||||
if(mSearchBox == null)
|
||||
{
|
||||
mSearchBox = new HBox();
|
||||
mSearchBox.setAlignment(Pos.CENTER_LEFT);
|
||||
mSearchBox.setSpacing(5);
|
||||
|
||||
Label searchLabel = new Label("Search:");
|
||||
searchLabel.setAlignment(Pos.CENTER_RIGHT);
|
||||
mSearchBox.getChildren().addAll(searchLabel, getSearchField(), getSearchColumnComboBox(), getSearchExactCheckBox());
|
||||
}
|
||||
|
||||
return mSearchBox;
|
||||
}
|
||||
|
||||
private TextField getSearchField()
|
||||
{
|
||||
if(mSearchField == null)
|
||||
{
|
||||
mSearchField = TextFields.createClearableTextField();
|
||||
mSearchField.setTooltip(new Tooltip("Type a value to search against the selected database field(s)"));
|
||||
mSearchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
showFirstPage();
|
||||
});
|
||||
}
|
||||
|
||||
return mSearchField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the calls table view for sort column and sort order changes to refresh the call event records.
|
||||
*/
|
||||
private class SortOrderListener implements ListChangeListener<TableColumn<Call, ?>>, ChangeListener
|
||||
{
|
||||
@Override
|
||||
public void onChanged(Change<? extends TableColumn<Call, ?>> c)
|
||||
{
|
||||
updateSortOrder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue observable, Object oldValue, Object newValue)
|
||||
{
|
||||
updateSortOrder();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles call events from the AudioManager.
|
||||
*/
|
||||
private class CallEventListener implements ICallEventListener
|
||||
{
|
||||
@Override
|
||||
public void added(Call call)
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
//Only update the calls table if we're viewing page 1
|
||||
if(getPagingController().getCurrentPage() == 1)
|
||||
{
|
||||
if(mCalls.size() == getPageSize())
|
||||
{
|
||||
mCalls.remove(mCalls.size() - 1);
|
||||
}
|
||||
|
||||
mCalls.add(0, call);
|
||||
|
||||
getCallTableView().sort();
|
||||
|
||||
//If the user hasn't selected an item, scroll to the new item.
|
||||
if(getCallTableView().getSelectionModel().getSelectedItem() != null)
|
||||
{
|
||||
getCallTableView().scrollTo(call);
|
||||
}
|
||||
}
|
||||
|
||||
mCallCount.set(mCallCount.get() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Call call)
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
if(mCalls.contains(call))
|
||||
{
|
||||
mCalls.set(mCalls.indexOf(call), call);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completed(Call call, AudioSegment audioSegment)
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
if(mCalls.contains(call))
|
||||
{
|
||||
mCalls.set(mCalls.indexOf(call), call);
|
||||
}
|
||||
});
|
||||
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleted(Call call)
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
if(mCalls.contains(call))
|
||||
{
|
||||
mCalls.remove(call);
|
||||
}
|
||||
|
||||
mCallCount.set(mCallCount.get() - 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class PriorityCellFactory implements Callback<TableColumn<Alias, Integer>, TableCell<Alias, Integer>>
|
||||
{
|
||||
@Override
|
||||
public TableCell<Alias, Integer> call(TableColumn<Alias, Integer> param)
|
||||
{
|
||||
TableCell tableCell = new TableCell<Alias, Integer>()
|
||||
{
|
||||
@Override
|
||||
protected void updateItem(Integer item, boolean empty)
|
||||
{
|
||||
if(empty)
|
||||
{
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
}
|
||||
else if(item == io.github.dsheirer.alias.id.priority.Priority.DO_NOT_MONITOR)
|
||||
{
|
||||
setText("Mute");
|
||||
final IconNode iconNode = new IconNode(FontAwesome.VOLUME_OFF);
|
||||
iconNode.setIconSize(20);
|
||||
iconNode.setFill(Color.RED);
|
||||
setGraphic(iconNode);
|
||||
}
|
||||
else if(item == io.github.dsheirer.alias.id.priority.Priority.DEFAULT_PRIORITY)
|
||||
{
|
||||
setText("Default");
|
||||
final IconNode iconNode = new IconNode(FontAwesome.VOLUME_UP);
|
||||
iconNode.setIconSize(20);
|
||||
iconNode.setFill(Color.GREEN);
|
||||
setGraphic(iconNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
setText(item.toString());
|
||||
final IconNode iconNode = new IconNode(FontAwesome.VOLUME_UP);
|
||||
iconNode.setIconSize(20);
|
||||
iconNode.setFill(Color.GREEN);
|
||||
setGraphic(iconNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return tableCell;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.call;
|
||||
|
||||
/**
|
||||
* Completed call record
|
||||
* @param call associated with the audio segment.
|
||||
* @param audioSegment associated with the call.
|
||||
*/
|
||||
public record CompletedCall(Call call, AudioSegment audioSegment)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.call;
|
||||
|
||||
/**
|
||||
* Call event listener interface
|
||||
*/
|
||||
public interface ICallEventListener
|
||||
{
|
||||
/**
|
||||
* Call added event.
|
||||
* @param call that was added
|
||||
*/
|
||||
void added(Call call);
|
||||
|
||||
/**
|
||||
* Call updated event.
|
||||
* @param call that was updated
|
||||
*/
|
||||
void updated(Call call);
|
||||
|
||||
/**
|
||||
* Call completed event. This indicates that the call was completed and no further updates will be available.
|
||||
* @param call that was completed.
|
||||
* @param audioSegment for the call
|
||||
*/
|
||||
void completed(Call call, AudioSegment audioSegment);
|
||||
|
||||
/**
|
||||
* Call that was deleted (age-off) from the database.
|
||||
* @param call that was deleted.
|
||||
*/
|
||||
void deleted(Call call);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.call;
|
||||
|
||||
/**
|
||||
* Listener interface for receiving paging requests from user initiated actions.
|
||||
*/
|
||||
public interface IPageRequestListener
|
||||
{
|
||||
/**
|
||||
* Indicates the user requests to display a page number
|
||||
* @param page number to display.
|
||||
*/
|
||||
void showPage(int page);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.call;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.javafx.IconNode;
|
||||
|
||||
/**
|
||||
* Implements a set of paging controls.
|
||||
*/
|
||||
public class PagingController extends HBox
|
||||
{
|
||||
private static final int ICON_SIZE = 20;
|
||||
private NumberFormat mNumberFormat = NumberFormat.getIntegerInstance();
|
||||
private Button mFirstButton;
|
||||
private Button mLastButton;
|
||||
private Button mNextButton;
|
||||
private Button mPreviousButton;
|
||||
private Label mPageLabel;
|
||||
private int mPageCount = 1;
|
||||
private int mCurrentPage = 1;
|
||||
private IPageRequestListener mPageRequestListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param listener to receive page requests
|
||||
*/
|
||||
public PagingController(IPageRequestListener listener)
|
||||
{
|
||||
mPageRequestListener = listener;
|
||||
setAlignment(Pos.CENTER);
|
||||
setSpacing(0);
|
||||
getChildren().addAll(getFirstButton(), getPreviousButton(), getPageLabel(), getNextButton(), getLastButton());
|
||||
update(1, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current state of this paging controller.
|
||||
* @param currentPage number to display.
|
||||
* @param pageCount the total number of available pages.
|
||||
*/
|
||||
public void update(int currentPage, int pageCount)
|
||||
{
|
||||
mPageCount = pageCount;
|
||||
mCurrentPage = currentPage;
|
||||
getFirstButton().setDisable(mCurrentPage <= 1);
|
||||
getFirstButton().setTooltip(mCurrentPage <= 1 ? null : new Tooltip("Move to page 1"));
|
||||
getPreviousButton().setDisable(mCurrentPage <= 1);
|
||||
getPreviousButton().setTooltip(mCurrentPage <= 1 ? null : new Tooltip("Move to page " + (mCurrentPage - 1)));
|
||||
getNextButton().setDisable(mCurrentPage >= mPageCount);
|
||||
getNextButton().setTooltip(mCurrentPage >= mPageCount ? null : new Tooltip("Move to page " + (mCurrentPage + 1)));
|
||||
getLastButton().setDisable(mCurrentPage >= mPageCount);
|
||||
getLastButton().setTooltip(mCurrentPage >= mPageCount ? null : new Tooltip("Move to page " + mPageCount));
|
||||
getPageLabel().setText("Page " + mNumberFormat.format(mCurrentPage) + " of " + mNumberFormat.format(mPageCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Total pages available
|
||||
* @return page count
|
||||
*/
|
||||
public int getPageCount()
|
||||
{
|
||||
return mPageCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current page number
|
||||
* @return current page.
|
||||
*/
|
||||
public int getCurrentPage()
|
||||
{
|
||||
return mCurrentPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the listener that the user requests the page number
|
||||
* @param page number requested
|
||||
*/
|
||||
private void request(int page)
|
||||
{
|
||||
if(mPageRequestListener != null)
|
||||
{
|
||||
mPageRequestListener.showPage(page);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Next page
|
||||
*/
|
||||
private void next()
|
||||
{
|
||||
request(mCurrentPage + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Previous page
|
||||
*/
|
||||
private void previous()
|
||||
{
|
||||
request(mCurrentPage - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* First page
|
||||
*/
|
||||
private void first()
|
||||
{
|
||||
request(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Last page
|
||||
*/
|
||||
private void last()
|
||||
{
|
||||
request(mPageCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Current page status label.
|
||||
*/
|
||||
private Label getPageLabel()
|
||||
{
|
||||
if(mPageLabel == null)
|
||||
{
|
||||
mPageLabel = new Label("Page 1 of 1");
|
||||
mPageLabel.setTooltip(new Tooltip("Current Page and Total Page Count"));
|
||||
mPageLabel.setAlignment(Pos.CENTER);
|
||||
mPageLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
mPageLabel.setPadding(new Insets(0, 20, 0, 20));
|
||||
}
|
||||
|
||||
return mPageLabel;
|
||||
}
|
||||
|
||||
/**
|
||||
* First page button.
|
||||
*/
|
||||
private Button getFirstButton()
|
||||
{
|
||||
if(mFirstButton == null)
|
||||
{
|
||||
mFirstButton = new Button();
|
||||
mFirstButton.setTooltip(new Tooltip("Go To First Page"));
|
||||
IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_LEFT);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mFirstButton.setGraphic(iconNode);
|
||||
mFirstButton.setOnAction(event -> first());
|
||||
}
|
||||
|
||||
return mFirstButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last page button
|
||||
*/
|
||||
private Button getLastButton()
|
||||
{
|
||||
if(mLastButton == null)
|
||||
{
|
||||
mLastButton = new Button();
|
||||
mLastButton.setTooltip(new Tooltip("Go To Last Page"));
|
||||
IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_RIGHT);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mLastButton.setGraphic(iconNode);
|
||||
mLastButton.setOnAction(event -> last());
|
||||
}
|
||||
|
||||
return mLastButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Next page button
|
||||
*/
|
||||
private Button getNextButton()
|
||||
{
|
||||
if(mNextButton == null)
|
||||
{
|
||||
mNextButton = new Button();
|
||||
mNextButton.setTooltip(new Tooltip("Go To Next Page"));
|
||||
IconNode iconNode = new IconNode(FontAwesome.ANGLE_RIGHT);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mNextButton.setGraphic(iconNode);
|
||||
mNextButton.setOnAction(event -> next());
|
||||
}
|
||||
|
||||
return mNextButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Previous page button.
|
||||
*/
|
||||
private Button getPreviousButton()
|
||||
{
|
||||
if(mPreviousButton == null)
|
||||
{
|
||||
mPreviousButton = new Button();
|
||||
mPreviousButton.setTooltip(new Tooltip("Go To Previous Page"));
|
||||
IconNode iconNode = new IconNode(FontAwesome.ANGLE_LEFT);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.BLACK);
|
||||
mPreviousButton.setGraphic(iconNode);
|
||||
mPreviousButton.setOnAction(event -> previous());
|
||||
}
|
||||
|
||||
return mPreviousButton;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.call;
|
||||
|
||||
/**
|
||||
* Search columns
|
||||
*/
|
||||
public enum SearchColumn
|
||||
{
|
||||
ANY_ID("Any ID"),
|
||||
TO("To / Alias"),
|
||||
FROM("From / Alias"),
|
||||
CHANNEL("Channel"),
|
||||
PROTOCOL("Protocol"),
|
||||
SITE("Site"),
|
||||
SYSTEM("System"),
|
||||
CALL_TYPE("Type");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param label to display
|
||||
*/
|
||||
private SearchColumn(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display label
|
||||
*/
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -19,16 +19,17 @@
|
||||
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.audio.AbstractAudioModule;
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.IMessageListener;
|
||||
import io.github.dsheirer.preference.IPreferenceUpdateListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
@@ -42,28 +43,38 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
|
||||
ISquelchStateListener
|
||||
ISquelchStateListener, IPreferenceUpdateListener
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
|
||||
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
|
||||
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
|
||||
private IAudioCodec mAudioCodec;
|
||||
private final UserPreferences mUserPreferences;
|
||||
private UserPreferences mUserPreferences;
|
||||
private static Class sLoadedJmbeAudioConverterClass;
|
||||
|
||||
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
|
||||
{
|
||||
super(aliasList, timeslot, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
|
||||
mUserPreferences = userPreferences;
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
loadConverter();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init()
|
||||
{
|
||||
mUserPreferences.addUpdateListener(this);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy()
|
||||
{
|
||||
mUserPreferences.removeUpdateListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
mAudioCodec = null;
|
||||
}
|
||||
|
||||
@@ -91,7 +102,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
*
|
||||
* @param preferenceType that was updated
|
||||
*/
|
||||
@Subscribe
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
if(preferenceType == PreferenceType.JMBE_LIBRARY)
|
||||
|
||||
@@ -21,7 +21,7 @@ package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
|
||||
@@ -30,17 +30,11 @@ import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifi
|
||||
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioSegmentRecorder;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Collection;
|
||||
import jmbe.iface.IAudioCodec;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.filefilter.DirectoryFileFilter;
|
||||
import org.apache.commons.io.filefilter.SuffixFileFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -57,15 +51,15 @@ public class MBECallSequenceConverter
|
||||
* @param output path to write the WAVE recording.
|
||||
* @throws IOException if there is an error.
|
||||
*/
|
||||
public static void convert(Path input, Path output) throws IOException
|
||||
public static void convert(UserPreferences userPreferences, Path input, Path output) throws IOException
|
||||
{
|
||||
InputStream inputStream = Files.newInputStream(input);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
|
||||
convert(sequence, output);
|
||||
convert(userPreferences, sequence, output);
|
||||
}
|
||||
|
||||
public static void convert(MBECallSequence callSequence, Path outputPath)
|
||||
public static void convert(UserPreferences userPreferences, MBECallSequence callSequence, Path outputPath)
|
||||
{
|
||||
if(callSequence == null || callSequence.isEncrypted())
|
||||
{
|
||||
@@ -76,7 +70,7 @@ public class MBECallSequenceConverter
|
||||
{
|
||||
if(callSequence.getProtocol().equals(P25P1CallSequenceRecorder.PROTOCOL))
|
||||
{
|
||||
P25P1AudioModule audioModule = new P25P1AudioModule(new UserPreferences(), new AliasList("mbe generator"));
|
||||
P25P1AudioModule audioModule = new P25P1AudioModule(userPreferences, new AliasList("mbe generator"));
|
||||
audioModule.setRecordAudio(true);
|
||||
audioModule.start();
|
||||
|
||||
@@ -136,45 +130,4 @@ public class MBECallSequenceConverter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
boolean all = true;
|
||||
|
||||
String path = "/home/denny/SDRTrunk/recordings";
|
||||
Path input = Paths.get(path);
|
||||
|
||||
if(all)
|
||||
{
|
||||
Collection<File> mbeFiles = FileUtils.listFiles(input.toFile(), new SuffixFileFilter(".mbe"), DirectoryFileFilter.DIRECTORY);
|
||||
|
||||
for(File inputFile: mbeFiles)
|
||||
{
|
||||
Path output = Paths.get(inputFile.getAbsolutePath().replace(".mbe", ".wav"));
|
||||
mLog.info("Converting: " + inputFile);
|
||||
try
|
||||
{
|
||||
MBECallSequenceConverter.convert(inputFile.toPath(), output);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Path output = Paths.get(path.replace(".mbe", ".wav"));
|
||||
mLog.info("Converting: " + path);
|
||||
|
||||
try
|
||||
{
|
||||
MBECallSequenceConverter.convert(input, output);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.playback;
|
||||
|
||||
@@ -25,12 +24,12 @@ import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.preference.IPreferenceUpdateListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
|
||||
@@ -40,13 +39,6 @@ import io.github.dsheirer.settings.ColorSetting;
|
||||
import io.github.dsheirer.settings.Setting;
|
||||
import io.github.dsheirer.settings.SettingChangeListener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Color;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Font;
|
||||
@@ -54,8 +46,15 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener, IPreferenceUpdateListener
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioChannelPanel.class);
|
||||
@@ -66,53 +65,54 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
public static final String PROPERTY_COLOR_MUTED = PROPERTY_PREFIX + "muted";
|
||||
public static final String PROPERTY_COLOR_VALUE = PROPERTY_PREFIX + "value";
|
||||
|
||||
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
|
||||
private AliasModel mAliasModel;
|
||||
private IconModel mIconModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
|
||||
private Color mBackgroundColor;
|
||||
private Color mLabelColor;
|
||||
private Color mMutedColor;
|
||||
private Color mValueColor;
|
||||
|
||||
private IconModel mIconModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
private TalkgroupFormatPreference mTalkgroupFormatPreference;
|
||||
private AudioOutput mAudioOutput;
|
||||
|
||||
private JLabel mMutedLabel = new JLabel("M");
|
||||
private JLabel mChannelName = new JLabel(" ");
|
||||
private JLabel mIconLabel = new JLabel(" ");
|
||||
private JLabel mIdentifierLabel = new JLabel("-----");
|
||||
private Identifier mIdentifier;
|
||||
private List<Alias> mAliases = Collections.EMPTY_LIST;
|
||||
private AliasModel mAliasModel;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
|
||||
public AudioChannelPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
AudioOutput audioOutput, AliasModel aliasModel)
|
||||
public AudioChannelPanel(UserPreferences userPreferences, SettingsManager settingsManager, AliasModel aliasModel,
|
||||
IconModel iconModel, AudioOutput audioOutput)
|
||||
{
|
||||
mIconModel = iconModel;
|
||||
mSettingsManager = settingsManager;
|
||||
mSettingsManager.addListener(this);
|
||||
mAliasModel = aliasModel;
|
||||
mUserPreferences = userPreferences;
|
||||
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
|
||||
mSettingsManager = settingsManager;
|
||||
mAliasModel = aliasModel;
|
||||
mAudioOutput = audioOutput;
|
||||
|
||||
mIconModel = iconModel;
|
||||
mSettingsManager.addListener(this);
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.addAudioEventListener(this);
|
||||
mAudioOutput.setIdentifierCollectionListener(new AudioMetadataProcessor());
|
||||
}
|
||||
|
||||
mUserPreferences.addUpdateListener(this);
|
||||
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
|
||||
mBackgroundColor = SystemProperties.getInstance().get(PROPERTY_COLOR_BACKGROUND, Color.BLACK);
|
||||
mLabelColor = SystemProperties.getInstance().get(PROPERTY_COLOR_LABEL, Color.LIGHT_GRAY);
|
||||
mMutedColor = SystemProperties.getInstance().get(PROPERTY_COLOR_MUTED, Color.RED);
|
||||
mValueColor = SystemProperties.getInstance().get(PROPERTY_COLOR_VALUE, Color.GREEN);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public void preDestroy()
|
||||
{
|
||||
mUserPreferences.removeUpdateListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives preference update notifications via the event bus
|
||||
* @param preferenceType that was updated
|
||||
@@ -128,9 +128,6 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
//Deregister from receiving preference update notifications
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.removeAudioEventListener(this);
|
||||
@@ -140,9 +137,6 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
|
||||
private void init()
|
||||
{
|
||||
//Register to receive preference updates
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
|
||||
setLayout(new MigLayout("align center center, insets 0 0 0 0",
|
||||
"[][][align right]0[grow,fill]", ""));
|
||||
setBackground(mBackgroundColor);
|
||||
@@ -183,14 +177,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
break;
|
||||
case AUDIO_MUTED:
|
||||
case AUDIO_UNMUTED:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mMutedLabel.setVisible(mAudioOutput.isMuted());
|
||||
}
|
||||
});
|
||||
EventQueue.invokeLater(() -> mMutedLabel.setVisible(mAudioOutput.isMuted()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.playback;
|
||||
|
||||
@@ -24,20 +23,20 @@ import io.github.dsheirer.audio.IAudioController;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.util.List;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSeparator;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.util.List;
|
||||
|
||||
public class AudioChannelsPanel extends JPanel
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public AudioChannelsPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
IAudioController controller, AliasModel aliasModel)
|
||||
public AudioChannelsPanel(IAudioController controller, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
AliasModel aliasModel, IconModel iconModel)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0",
|
||||
"[][sizegroup abc,grow,fill][][sizegroup abc,grow,fill]", "[grow,fill]"));
|
||||
@@ -50,7 +49,7 @@ public class AudioChannelsPanel extends JPanel
|
||||
|
||||
for(int x = 0; x < outputs.size(); x++)
|
||||
{
|
||||
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, outputs.get(x), aliasModel));
|
||||
add(new AudioChannelPanel(userPreferences, settingsManager, aliasModel, iconModel, outputs.get(x)));
|
||||
|
||||
if(x < outputs.size() - 1)
|
||||
{
|
||||
@@ -63,7 +62,7 @@ public class AudioChannelsPanel extends JPanel
|
||||
if(outputs.size() == 1)
|
||||
{
|
||||
addSeparator();
|
||||
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, null, aliasModel), "growx");
|
||||
add(new AudioChannelPanel(userPreferences, settingsManager, aliasModel, iconModel, null), "growx");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.controller.NamingThreadFactory;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.preference.IPreferenceUpdateListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
@@ -55,7 +54,7 @@ import javax.sound.sampled.SourceDataLine;
|
||||
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
|
||||
* and broadcasts audio segment metadata to registered listeners (ie gui components).
|
||||
*/
|
||||
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
|
||||
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>, IPreferenceUpdateListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
private int mBufferStartThreshold;
|
||||
@@ -82,6 +81,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
private boolean mDropDuplicates;
|
||||
private long mOutputLastTimestamp = 0;
|
||||
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
|
||||
private AudioFormat mAudioFormat;
|
||||
private Line.Info mLineInfo;
|
||||
private int mRequestedBufferSize;
|
||||
|
||||
/**
|
||||
* Single audio channel playback with automatic starting and stopping of the
|
||||
@@ -102,18 +104,31 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
{
|
||||
mMixer = mixer;
|
||||
mMixerChannel = mixerChannel;
|
||||
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
||||
"sdrtrunk audio output " + mixerChannel.name()));
|
||||
mAudioFormat = audioFormat;
|
||||
mLineInfo = lineInfo;
|
||||
mRequestedBufferSize = requestedBufferSize;
|
||||
mUserPreferences = userPreferences;
|
||||
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts and configures this audio output
|
||||
*/
|
||||
private void start()
|
||||
{
|
||||
mUserPreferences.addUpdateListener(this);
|
||||
|
||||
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
||||
"sdrtrunk audio output " + mMixerChannel.name()));
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = (SourceDataLine) mMixer.getLine(lineInfo);
|
||||
mOutput = (SourceDataLine) mMixer.getLine(mLineInfo);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
mOutput.open(audioFormat, requestedBufferSize);
|
||||
mOutput.open(mAudioFormat, mRequestedBufferSize);
|
||||
|
||||
//Start threshold: buffer is full with 10% or less of capacity remaining
|
||||
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
|
||||
@@ -133,7 +148,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -144,12 +159,12 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
//Run the queue processor task every 100 milliseconds or 10 times a second
|
||||
//Run the queue processor task every 100 milliseconds or 10 times a second
|
||||
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||
@@ -160,13 +175,10 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
}
|
||||
|
||||
updateToneInsertionAudioClips();
|
||||
|
||||
//Register to receive preference update notifications so we can update the preference items
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,7 +233,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
/**
|
||||
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
|
||||
*/
|
||||
@Subscribe
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
@@ -230,7 +241,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
}
|
||||
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
|
||||
{
|
||||
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +315,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
{
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
mCurrentAudioSegment.decrementConsumerCount();
|
||||
mCurrentAudioSegment.removeLease(getClass().toString());
|
||||
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
|
||||
mCurrentAudioSegment = null;
|
||||
broadcast(null);
|
||||
@@ -327,7 +338,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
|
||||
if(isThrowaway(audioSegment))
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
audioSegment = mAudioSegmentQueue.poll();
|
||||
}
|
||||
else
|
||||
@@ -440,7 +451,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
mCanProcessAudio = false;
|
||||
|
||||
if(mProcessorFuture != null)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -21,7 +21,6 @@ package io.github.dsheirer.audio.playback;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioException;
|
||||
import io.github.dsheirer.audio.IAudioController;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
@@ -30,6 +29,15 @@ import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.Color;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
@@ -49,61 +57,51 @@ import javax.swing.JSlider;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Color;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
|
||||
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
|
||||
|
||||
private static ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
|
||||
private static ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
|
||||
|
||||
private IconModel mIconModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
private IAudioController mController;
|
||||
private AliasModel mAliasModel;
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
private JButton mMuteButton;
|
||||
private AudioChannelsPanel mAudioChannelsPanel;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
@Resource
|
||||
private AudioPlaybackManager mAudioPlaybackManager;
|
||||
@Resource
|
||||
private IconModel mIconModel;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
@Resource
|
||||
private SettingsManager mSettingsManager;
|
||||
|
||||
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
IAudioController controller, AliasModel aliasModel)
|
||||
public AudioPanel()
|
||||
{
|
||||
mIconModel = iconModel;
|
||||
mSettingsManager = settingsManager;
|
||||
mController = controller;
|
||||
mAliasModel = aliasModel;
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
mController.addControllerListener(this);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init()
|
||||
{
|
||||
mAudioPlaybackManager.addControllerListener(this);
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
|
||||
setBackground(Color.BLACK);
|
||||
|
||||
mMuteButton = new MuteButton();
|
||||
mMuteButton.setBackground(getBackground());
|
||||
add(mMuteButton);
|
||||
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
|
||||
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mAudioPlaybackManager, mUserPreferences, mSettingsManager,
|
||||
mAliasModel, mIconModel);
|
||||
add(mAudioChannelsPanel);
|
||||
|
||||
addMouseListener(new MouseSelectionListener());
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void preDestroy()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioEvent event)
|
||||
{
|
||||
@@ -112,19 +110,15 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
case AUDIO_CONFIGURATION_CHANGE_STARTED:
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
remove(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.dispose();
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
|
||||
add(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.repaint();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
EventQueue.invokeLater(() -> {
|
||||
remove(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.dispose();
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mAudioPlaybackManager, mUserPreferences, mSettingsManager,
|
||||
mAliasModel, mIconModel);
|
||||
add(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.repaint();
|
||||
revalidate();
|
||||
repaint();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -147,14 +141,7 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
|
||||
mAudioOutput = audioOutput;
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
mAudioOutput.setMuted(!mAudioOutput.isMuted());
|
||||
}
|
||||
});
|
||||
addActionListener(e -> mAudioOutput.setMuted(!mAudioOutput.isMuted()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,14 +166,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
|
||||
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_PLAYBACK));
|
||||
}
|
||||
});
|
||||
popup.add(outputMenu);
|
||||
popup.add(new JPopupMenu.Separator());
|
||||
|
||||
/* Audio output mute and volume control */
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
for(AudioOutput output : mAudioPlaybackManager.getAudioOutputs())
|
||||
{
|
||||
JMenu menu = new JMenu("Channel: " + output.getChannelName());
|
||||
|
||||
@@ -353,7 +340,7 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
{
|
||||
try
|
||||
{
|
||||
mController.setMixerChannelConfiguration(mConfiguration);
|
||||
mAudioPlaybackManager.setMixerChannelConfiguration(mConfiguration);
|
||||
}
|
||||
catch(AudioException e1)
|
||||
{
|
||||
@@ -392,7 +379,7 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
{
|
||||
mMuted = !mMuted;
|
||||
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
for(AudioOutput output : mAudioPlaybackManager.getAudioOutputs())
|
||||
{
|
||||
output.setMuted(mMuted);
|
||||
}
|
||||
|
||||
@@ -18,19 +18,21 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioException;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.audio.IAudioController;
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.controller.NamingThreadFactory;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.IPreferenceUpdateListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
@@ -43,20 +45,22 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Manages scheduling and playback of audio segments to the local users audio system.
|
||||
*/
|
||||
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
|
||||
@Component("audioPlaybackManager")
|
||||
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController, IPreferenceUpdateListener
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
||||
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||
private ScheduledFuture<?> mProcessingTask;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
||||
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
|
||||
@@ -68,13 +72,15 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
*
|
||||
* @param userPreferences for audio playback preferences
|
||||
*/
|
||||
public AudioPlaybackManager(UserPreferences userPreferences)
|
||||
public AudioPlaybackManager()
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
mUserPreferences.addUpdateListener(this);
|
||||
|
||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||
|
||||
@@ -87,8 +93,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
catch(AudioException ae)
|
||||
{
|
||||
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
|
||||
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
|
||||
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
|
||||
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
|
||||
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -97,6 +103,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void preDestroy()
|
||||
{
|
||||
mUserPreferences.removeUpdateListener(this);
|
||||
dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives audio segments from channel audio modules.
|
||||
* @param audioSegment
|
||||
@@ -120,9 +133,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
while(newSegment != null)
|
||||
{
|
||||
if(newSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
{
|
||||
newSegment.decrementConsumerCount();
|
||||
newSegment.removeLease(getClass().toString());
|
||||
}
|
||||
else if(newSegment.hasAudio())
|
||||
{
|
||||
@@ -148,10 +161,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
else if(audioSegment.hasAudio())
|
||||
{
|
||||
@@ -163,7 +176,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
//Rare situation: the audio segment completed but never had audio ... dispose it
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,10 +195,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
else if(audioSegment.isLinked())
|
||||
{
|
||||
@@ -194,7 +207,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.addLease(audioOutput.getClass().toString());
|
||||
audioOutput.play(audioSegment);
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +226,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
if(audioOutput.isEmpty())
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
AudioSegment audioSegment1 = mAudioSegments.remove(0);
|
||||
audioSegment1.addLease(audioOutput.getClass().toString());
|
||||
audioOutput.play(audioSegment1);
|
||||
audioSegment1.removeLease(getClass().toString());
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
@@ -228,10 +246,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeLease(getClass().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +257,6 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
if(mProcessingTask != null)
|
||||
{
|
||||
mProcessingTask.cancel(true);
|
||||
@@ -254,7 +271,6 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
* Receive user preference update notifications so that we can detect when the user changes the audio output
|
||||
* device in the user preferences editor.
|
||||
*/
|
||||
@Subscribe
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -21,12 +21,12 @@ package io.github.dsheirer.audio.playback;
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
|
||||
/**
|
||||
* Mono Audio output implementation
|
||||
*/
|
||||
@@ -37,7 +37,7 @@ public class MonoAudioOutput extends AudioOutput
|
||||
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
|
||||
{
|
||||
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO,
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -21,12 +21,12 @@ package io.github.dsheirer.audio.playback;
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
|
||||
/**
|
||||
* Stereo audio output implementation.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,456 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
import io.github.dsheirer.audio.call.Call;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaException;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
|
||||
/**
|
||||
* Controller for a single audio playback channel.
|
||||
*/
|
||||
public class AudioPlaybackChannelController implements ChangeListener<MediaPlayer.Status>
|
||||
{
|
||||
private static final String EMPTY = new String("EMPTY");
|
||||
private static final String LOADED = new String("LOADED");
|
||||
private static final String NO_FILE = new String("NO FILE");
|
||||
|
||||
private ObjectProperty<MediaPlayer> mMediaPlayer = new SimpleObjectProperty<>();
|
||||
private StringProperty mFrequency = new SimpleStringProperty();
|
||||
private StringProperty mFrom = new SimpleStringProperty();
|
||||
private StringProperty mFromAlias = new SimpleStringProperty();
|
||||
private StringProperty mName = new SimpleStringProperty();
|
||||
private StringProperty mSite = new SimpleStringProperty();
|
||||
private StringProperty mSystem = new SimpleStringProperty();
|
||||
private StringProperty mTo = new SimpleStringProperty();
|
||||
private StringProperty mToAlias = new SimpleStringProperty();
|
||||
private ObjectProperty<PlaybackMode> mPlaybackMode = new SimpleObjectProperty<>();
|
||||
private StringProperty mMediaPlayerStatus = new SimpleStringProperty(EMPTY);
|
||||
private Call mCall;
|
||||
private double mBalance;
|
||||
private List<IAudioPlaybackStatusListener> mAudioPlaybackStatusListeners = new ArrayList<>();
|
||||
private EndOfMediaRunnable mEndOfMediaRunnable = new EndOfMediaRunnable();
|
||||
private CallUpdateListener mCallUpdateListener = new CallUpdateListener();
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param mixerChannel channel for this controller
|
||||
* @param balance to effect audio playback balance where -1.0 is full left and 1.0 is full right.
|
||||
*/
|
||||
public AudioPlaybackChannelController(MixerChannel mixerChannel, double balance)
|
||||
{
|
||||
nameProperty().set(mixerChannel.getLabel());
|
||||
playbackModeProperty().set(PlaybackMode.AUTO);
|
||||
mBalance = balance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Status of the media player.
|
||||
*/
|
||||
public StringProperty mediaPlayerStatusProperty()
|
||||
{
|
||||
return mMediaPlayerStatus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this controller is locked for playback of a specific identifier.
|
||||
*/
|
||||
public boolean isLocked()
|
||||
{
|
||||
return playbackModeProperty().get().equals(PlaybackMode.LOCKED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this controller is (already) locked to the specified call.
|
||||
* @param call to check for lock match.
|
||||
* @return true if this controller is locked for the system and identifier of the call.
|
||||
*/
|
||||
public boolean isLockedFor(Call call)
|
||||
{
|
||||
return playbackModeProperty().get().equals(PlaybackMode.LOCKED) &&
|
||||
toProperty().get() != null &&
|
||||
(toProperty().get().equals(call.getToId()) || toProperty().get().equals(call.getFromId())) &&
|
||||
((systemProperty().get() == null && call.getSystem() == null) ||
|
||||
(systemProperty().get() != null && systemProperty().get().equals(call.getSystem())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this controller is set for an autoplay mode (AUTO or LOCKED) and currently doesn't have a call assigned.
|
||||
* @return true if available and has autoplay mode.
|
||||
*/
|
||||
public boolean isAvailableForAutoPlay()
|
||||
{
|
||||
return playbackModeProperty().get().isAutoPlayable() && mCall == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to be notified of audio playback status changes.
|
||||
* @param listener to be registered.
|
||||
*/
|
||||
public void add(IAudioPlaybackStatusListener listener)
|
||||
{
|
||||
mAudioPlaybackStatusListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an audio playback status change to registered listeners.
|
||||
*/
|
||||
private void broadcastPlaybackStatus(MediaPlayer.Status previousStatus, MediaPlayer.Status currentStatus)
|
||||
{
|
||||
for(IAudioPlaybackStatusListener listener: mAudioPlaybackStatusListeners)
|
||||
{
|
||||
listener.playbackStatusUpdated(this, previousStatus, currentStatus);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an end-of-media event to registered listeners.
|
||||
*/
|
||||
private void broadcastEndOfMedia()
|
||||
{
|
||||
for(IAudioPlaybackStatusListener listener: mAudioPlaybackStatusListeners)
|
||||
{
|
||||
listener.endOfMedia(this);
|
||||
}
|
||||
|
||||
//If we're in AUTO or LOCKED modes, clear the media player at the end of playback.
|
||||
if(playbackModeProperty().get().isAutoPlayable())
|
||||
{
|
||||
clearMediaPlayerAndCall();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback mode to LOCKED for the identifier and system, or AUTO if the arguments are null.
|
||||
* @param identifier optional identifier for LOCKED playback mode.
|
||||
* @param system optional system name for LOCKED playback mode.
|
||||
*/
|
||||
public void lockAutoTo(String identifier, String system)
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
clearMediaPlayerAndCall();
|
||||
playbackModeProperty().set(identifier == null ? PlaybackMode.AUTO : PlaybackMode.LOCKED);
|
||||
toProperty().set(identifier);
|
||||
//Only set the system value if the identifier is non-null
|
||||
systemProperty().set(identifier != null ? system : null);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets playback mode to AUTO and clears any currently playing audio.
|
||||
*/
|
||||
public void auto()
|
||||
{
|
||||
lockAutoTo(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes this controller.
|
||||
*/
|
||||
public void mute()
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
clearMediaPlayerAndCall();
|
||||
playbackModeProperty().set(PlaybackMode.MUTE);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the specified call and sets playback mode to REPLAY
|
||||
* @param call to replay
|
||||
*/
|
||||
public void replay(final Call call)
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
if(call != null && !call.equals(mCall))
|
||||
{
|
||||
clearMediaPlayerAndCall();
|
||||
mCall = call;
|
||||
mCall.playbackChannelProperty().set(nameProperty().get());
|
||||
updateCallMetadata();
|
||||
playbackModeProperty().set(PlaybackMode.REPLAY);
|
||||
loadMediaPlayer();
|
||||
}
|
||||
else if(call == null)
|
||||
{
|
||||
clearMediaPlayerAndCall();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the specified call. If this controller is already playing a previous call, stops that call and proceeds
|
||||
* to play this call. Does not change the replay mode. At the end of media, closes the audio file and clears
|
||||
* the call and call metadata.
|
||||
* @param call to play
|
||||
*/
|
||||
public void play(final Call call)
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
if(call != null && !call.equals(mCall))
|
||||
{
|
||||
clearMediaPlayerAndCall();
|
||||
mCall = call;
|
||||
mCall.playbackChannelProperty().set(nameProperty().get());
|
||||
loadMediaPlayer();
|
||||
updateCallMetadata();
|
||||
}
|
||||
else if(call == null)
|
||||
{
|
||||
clearMediaPlayerAndCall();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadMediaPlayer()
|
||||
{
|
||||
if(mCall.getFile() != null)
|
||||
{
|
||||
File file = new File(mCall.getFile());
|
||||
|
||||
if(file.exists())
|
||||
{
|
||||
try
|
||||
{
|
||||
MediaPlayer mediaPlayer = new MediaPlayer(new Media(file.toURI().toString()));
|
||||
//Create a new media player
|
||||
mMediaPlayer.set(mediaPlayer);
|
||||
//Register status listener
|
||||
mMediaPlayer.get().statusProperty().addListener(this);
|
||||
//Register runnable for end-of-media event
|
||||
mMediaPlayer.get().onEndOfMediaProperty().set(mEndOfMediaRunnable);
|
||||
//Set audio channel balance
|
||||
mMediaPlayer.get().balanceProperty().set(mBalance);
|
||||
//Set the media player to auto-start replay
|
||||
mMediaPlayer.get().setAutoPlay(true);
|
||||
mMediaPlayerStatus.set(LOADED);
|
||||
}
|
||||
catch(MediaException me)
|
||||
{
|
||||
mMediaPlayer.set(null);
|
||||
mMediaPlayerStatus.set(NO_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
//Register a listener on the call's last updated property for update notifications.
|
||||
mCall.lastUpdatedProperty().addListener(mCallUpdateListener);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Stops playback and clears the media player and nullifies the call.
|
||||
*/
|
||||
private void clearMediaPlayerAndCall()
|
||||
{
|
||||
if(mCall != null)
|
||||
{
|
||||
mCall.lastUpdatedProperty().removeListener(mCallUpdateListener);
|
||||
mCall.playbackChannelProperty().set(null);
|
||||
}
|
||||
|
||||
if(mMediaPlayer.get() != null)
|
||||
{
|
||||
mMediaPlayer.get().statusProperty().removeListener(this);
|
||||
mMediaPlayer.get().onEndOfMediaProperty().set(null);
|
||||
mMediaPlayer.get().stop();
|
||||
mMediaPlayer.set(null);
|
||||
}
|
||||
|
||||
mCall = null;
|
||||
updateCallMetadata();
|
||||
mediaPlayerStatusProperty().set(EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Media player property for this controller.
|
||||
*/
|
||||
public ObjectProperty<MediaPlayer> mediaPlayerProperty()
|
||||
{
|
||||
return mMediaPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates any call metadata that has changed.
|
||||
*/
|
||||
private void updateCallMetadata()
|
||||
{
|
||||
if(mCall != null)
|
||||
{
|
||||
//Only change TO and SYSTEM when we're not in locked mode
|
||||
if(!playbackModeProperty().get().equals(PlaybackMode.LOCKED))
|
||||
{
|
||||
updateProperty(toProperty(), mCall.getToId());
|
||||
updateProperty(systemProperty(), mCall.getSystem());
|
||||
}
|
||||
|
||||
updateProperty(toAliasProperty(), mCall.getToAlias());
|
||||
updateProperty(fromProperty(), mCall.getFromId());
|
||||
updateProperty(fromAliasProperty(), mCall.getFromAlias());
|
||||
updateProperty(frequencyProperty(), String.valueOf(mCall.getFrequency()));
|
||||
updateProperty(siteProperty(), mCall.getSite());
|
||||
}
|
||||
else
|
||||
{
|
||||
//Only change TO and SYSTEM when we're not in locked mode
|
||||
if(!playbackModeProperty().get().equals(PlaybackMode.LOCKED))
|
||||
{
|
||||
updateProperty(toProperty(), null);
|
||||
updateProperty(systemProperty(), null);
|
||||
}
|
||||
|
||||
updateProperty(toAliasProperty(), null);
|
||||
updateProperty(fromProperty(), null);
|
||||
updateProperty(fromAliasProperty(), null);
|
||||
updateProperty(frequencyProperty(), null);
|
||||
updateProperty(siteProperty(), null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the specified property if empty or if the value argument is different.
|
||||
* @param property to update
|
||||
* @param value that is updated.
|
||||
*/
|
||||
private void updateProperty(StringProperty property, String value)
|
||||
{
|
||||
if(property.get() == null || !property.get().equals(value))
|
||||
{
|
||||
property.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* From identifier property
|
||||
*/
|
||||
public StringProperty fromProperty()
|
||||
{
|
||||
return mFrom;
|
||||
}
|
||||
|
||||
/**
|
||||
* From identifier alias property
|
||||
*/
|
||||
public StringProperty fromAliasProperty()
|
||||
{
|
||||
return mFromAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* To identifier property
|
||||
*/
|
||||
public StringProperty toProperty()
|
||||
{
|
||||
return mTo;
|
||||
}
|
||||
|
||||
/**
|
||||
* To identifier alias property
|
||||
*/
|
||||
public StringProperty toAliasProperty()
|
||||
{
|
||||
return mToAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel frequency property
|
||||
*/
|
||||
public StringProperty frequencyProperty()
|
||||
{
|
||||
return mFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Site property
|
||||
*/
|
||||
public StringProperty siteProperty()
|
||||
{
|
||||
return mSite;
|
||||
}
|
||||
|
||||
/**
|
||||
* System property
|
||||
*/
|
||||
public StringProperty systemProperty()
|
||||
{
|
||||
return mSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback controller name property
|
||||
*/
|
||||
public StringProperty nameProperty()
|
||||
{
|
||||
return mName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback mode property
|
||||
*/
|
||||
public ObjectProperty<PlaybackMode> playbackModeProperty()
|
||||
{
|
||||
return mPlaybackMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the change listener interface for MediaPlayer status changes.
|
||||
*/
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends MediaPlayer.Status> observable, MediaPlayer.Status previousStatus,
|
||||
MediaPlayer.Status currentStatus)
|
||||
{
|
||||
broadcastPlaybackStatus(previousStatus, currentStatus);
|
||||
mMediaPlayerStatus.set(currentStatus.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable to register with the media player to detect when the end of the media is detected.
|
||||
*/
|
||||
private class EndOfMediaRunnable implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
broadcastEndOfMedia();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the call's last updated property to effect updates to this controller about the call state.
|
||||
*/
|
||||
private class CallUpdateListener implements ChangeListener<Number>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
|
||||
{
|
||||
updateCallMetadata();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import jiconfont.icons.elusive.Elusive;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.javafx.IconNode;
|
||||
|
||||
/**
|
||||
* Abstract audio playback channel view
|
||||
*/
|
||||
public abstract class AudioPlaybackChannelView extends VBox
|
||||
{
|
||||
protected static final String CSS_STYLE_FONT_LARGE = "-fx-font-size:16;";
|
||||
protected static final String CSS_STYLE_FONT_LARGE_BOLD = CSS_STYLE_FONT_LARGE + "-fx-font-weight: bold;";
|
||||
protected static final String TOOLTIP_MUTED = "Audio channel muted. Click to unmute audio channel";
|
||||
protected static final String TOOLTIP_UNMUTED = "Audio channel not muted. Click to mute audio channel";
|
||||
protected static final String TOOLTIP_LOCKED = "Audio channel playback locked to talkgroup. Click to unlock audio channel";
|
||||
protected static final String TOOLTIP_UNLOCKED = "Audio channel playback unlocked. Click to lock audio channel to current talkgroup";
|
||||
protected static final Color ICON_COLOR = Color.BLACK;
|
||||
protected static final int ICON_SIZE = 20;
|
||||
private AudioPlaybackChannelController mController;
|
||||
private Label mFromLabel;
|
||||
private Label mFromAliasLabel;
|
||||
private Label mToLabel;
|
||||
private Label mToAliasLabel;
|
||||
private Label mPlaybackMode;
|
||||
private Label mMediaPlayerStatus;
|
||||
private Label mMuteLabel;
|
||||
private Label mLockLabel;
|
||||
private Label mSystemLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param controller to use for this view.
|
||||
*/
|
||||
public AudioPlaybackChannelView(AudioPlaybackChannelController controller)
|
||||
{
|
||||
mController = controller;
|
||||
getController().playbackModeProperty().addListener((observable, oldValue, newValue) -> updateLockMuteState());
|
||||
getToLabel().textProperty().addListener((observable, oldValue, newValue) -> updateLockMuteState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any actions related to resizing the width of the panel.
|
||||
*/
|
||||
public abstract void resetWidth();
|
||||
|
||||
/**
|
||||
* Controller for this view.
|
||||
* @return controller
|
||||
*/
|
||||
protected AudioPlaybackChannelController getController()
|
||||
{
|
||||
return mController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the state of the lock and mute labels (ie buttons).
|
||||
*/
|
||||
protected void updateLockMuteState()
|
||||
{
|
||||
switch(getController().playbackModeProperty().get())
|
||||
{
|
||||
case MUTE:
|
||||
setMuted(true);
|
||||
setLocked(false, true);
|
||||
break;
|
||||
case AUTO:
|
||||
case REPLAY:
|
||||
setMuted(false);
|
||||
setLocked(false, getToLabel().textProperty().get() == null);
|
||||
break;
|
||||
case LOCKED:
|
||||
setMuted(false);
|
||||
setLocked(true, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of the lock label.
|
||||
* @param locked true to show locked state, false otherwise
|
||||
* @param disabled true to disable the label, false otherwise
|
||||
*/
|
||||
private void setLocked(boolean locked, boolean disabled)
|
||||
{
|
||||
IconNode iconNode = new IconNode(locked ? Elusive.LOCK : Elusive.UNLOCK);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(disabled ? Color.DARKGRAY : ICON_COLOR);
|
||||
getLockLabel().setGraphic(iconNode);
|
||||
getLockLabel().setTooltip(new Tooltip(locked ? TOOLTIP_LOCKED : TOOLTIP_UNLOCKED));
|
||||
getLockLabel().setDisable(disabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the state of the mute label.
|
||||
* @param locked true to show muted state, false otherwise
|
||||
*/
|
||||
private void setMuted(boolean muted)
|
||||
{
|
||||
IconNode iconNode = new IconNode(muted ? Elusive.VOLUME_OFF : Elusive.VOLUME_UP);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(muted ? Color.RED : Color.BLACK);
|
||||
getMuteLabel().setGraphic(iconNode);
|
||||
getMuteLabel().setTooltip(new Tooltip(muted ? TOOLTIP_MUTED : TOOLTIP_UNMUTED));
|
||||
}
|
||||
|
||||
protected Label getFromLabel()
|
||||
{
|
||||
if(mFromLabel == null)
|
||||
{
|
||||
mFromLabel = new Label();
|
||||
mFromLabel.setStyle(CSS_STYLE_FONT_LARGE_BOLD);
|
||||
mFromLabel.textProperty().bind(getController().fromProperty());
|
||||
}
|
||||
|
||||
return mFromLabel;
|
||||
}
|
||||
|
||||
protected Label getFromAliasLabel()
|
||||
{
|
||||
if(mFromAliasLabel == null)
|
||||
{
|
||||
mFromAliasLabel = new Label();
|
||||
mFromAliasLabel.setStyle(CSS_STYLE_FONT_LARGE);
|
||||
mFromAliasLabel.textProperty().bind(getController().fromAliasProperty());
|
||||
}
|
||||
|
||||
return mFromAliasLabel;
|
||||
}
|
||||
|
||||
protected Label getSystemLabel()
|
||||
{
|
||||
if(mSystemLabel == null)
|
||||
{
|
||||
mSystemLabel = new Label();
|
||||
mSystemLabel.textProperty().bind(getController().systemProperty());
|
||||
}
|
||||
|
||||
return mSystemLabel;
|
||||
}
|
||||
|
||||
protected Label getToLabel()
|
||||
{
|
||||
if(mToLabel == null)
|
||||
{
|
||||
mToLabel = new Label();
|
||||
mToLabel.setStyle(CSS_STYLE_FONT_LARGE_BOLD);
|
||||
mToLabel.textProperty().bind(getController().toProperty());
|
||||
}
|
||||
|
||||
return mToLabel;
|
||||
}
|
||||
|
||||
protected Label getToAliasLabel()
|
||||
{
|
||||
if(mToAliasLabel == null)
|
||||
{
|
||||
mToAliasLabel = new Label();
|
||||
mToAliasLabel.setStyle(CSS_STYLE_FONT_LARGE);
|
||||
mToAliasLabel.textProperty().bind(getController().toAliasProperty());
|
||||
}
|
||||
|
||||
return mToAliasLabel;
|
||||
}
|
||||
|
||||
protected Label getMuteLabel()
|
||||
{
|
||||
if(mMuteLabel == null)
|
||||
{
|
||||
mMuteLabel = new Label();
|
||||
mMuteLabel.setTooltip(new Tooltip(TOOLTIP_UNMUTED));
|
||||
mMuteLabel.setPadding(new Insets(1));
|
||||
IconNode iconNode = new IconNode(Elusive.VOLUME_UP);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(ICON_COLOR);
|
||||
mMuteLabel.setGraphic(iconNode);
|
||||
mMuteLabel.setOnMouseClicked(event -> {
|
||||
if(getController().playbackModeProperty().get().equals(PlaybackMode.MUTE))
|
||||
{
|
||||
getController().auto();
|
||||
}
|
||||
else
|
||||
{
|
||||
getController().mute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mMuteLabel;
|
||||
}
|
||||
|
||||
protected Label getLockLabel()
|
||||
{
|
||||
if(mLockLabel == null)
|
||||
{
|
||||
mLockLabel = new Label();
|
||||
mLockLabel.setPadding(new Insets(1));
|
||||
IconNode iconNode = new IconNode(FontAwesome.UNLOCK);
|
||||
iconNode.setIconSize(ICON_SIZE);
|
||||
iconNode.setFill(Color.DARKGRAY);
|
||||
mLockLabel.setGraphic(iconNode);
|
||||
mLockLabel.setOnMouseClicked(event -> {
|
||||
if(getController().playbackModeProperty().get().equals(PlaybackMode.LOCKED))
|
||||
{
|
||||
getController().auto();
|
||||
}
|
||||
else
|
||||
{
|
||||
String to = getToLabel().textProperty().get();
|
||||
String system = getSystemLabel().textProperty().get();
|
||||
getController().lockAutoTo(to, system);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mLockLabel;
|
||||
}
|
||||
|
||||
protected Label getPlaybackMode()
|
||||
{
|
||||
if(mPlaybackMode == null)
|
||||
{
|
||||
mPlaybackMode = new Label();
|
||||
mPlaybackMode.textProperty().bind(getController().playbackModeProperty().map(playbackMode -> switch(playbackMode)
|
||||
{
|
||||
case AUTO -> "AUTO";
|
||||
case LOCKED -> "AUTO-LOCKED";
|
||||
case REPLAY -> "REPLAY";
|
||||
case MUTE -> "MUTED";
|
||||
}));
|
||||
}
|
||||
|
||||
return mPlaybackMode;
|
||||
}
|
||||
|
||||
protected Label getMediaPlayerStatus()
|
||||
{
|
||||
if(mMediaPlayerStatus == null)
|
||||
{
|
||||
mMediaPlayerStatus = new Label();
|
||||
mMediaPlayerStatus.setDisable(true);
|
||||
mMediaPlayerStatus.textProperty().bind(getController().mediaPlayerStatusProperty());
|
||||
}
|
||||
|
||||
return mMediaPlayerStatus;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Complete audio playback view
|
||||
*/
|
||||
public class AudioPlaybackChannelViewComplete extends AudioPlaybackChannelView
|
||||
{
|
||||
private AudioSpectrumView mAudioSpectrumView;
|
||||
private Label mFrequencyLabel;
|
||||
private Label mNameLabel;
|
||||
private Label mSiteLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param controller for audio playback that backs this view.
|
||||
*/
|
||||
public AudioPlaybackChannelViewComplete(AudioPlaybackChannelController controller)
|
||||
{
|
||||
super(controller);
|
||||
//Bind the audio spectrum view to the controller's media player so that it can update as the playback changes
|
||||
getAudioSpectrumView().mediaPlayerProperty().bind(getController().mediaPlayerProperty());
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the spectrum width during resizing.
|
||||
*/
|
||||
public void resetWidth()
|
||||
{
|
||||
getAudioSpectrumView().resetWidth();
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setSpacing(3);
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
setBorder(new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, null, null)));
|
||||
setPadding(new Insets(0));
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setPadding(new Insets(0, 0, 2, 2));
|
||||
gridPane.setHgap(3);
|
||||
|
||||
int row = 0;
|
||||
|
||||
GridPane.setHalignment(getNameLabel(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getNameLabel(), Priority.NEVER);
|
||||
gridPane.add(getNameLabel(), 0, row, 2, 1);
|
||||
|
||||
HBox playbackStatusIdentifierBox = new HBox();
|
||||
playbackStatusIdentifierBox.setAlignment(Pos.CENTER);
|
||||
HBox.setHgrow(getMediaPlayerStatus(), Priority.ALWAYS);
|
||||
getMediaPlayerStatus().setMaxWidth(Double.MAX_VALUE);
|
||||
getMediaPlayerStatus().setAlignment(Pos.BASELINE_RIGHT);
|
||||
playbackStatusIdentifierBox.getChildren().addAll(getPlaybackMode(), getMediaPlayerStatus());
|
||||
|
||||
GridPane.setHalignment(playbackStatusIdentifierBox, HPos.LEFT);
|
||||
GridPane.setHgrow(playbackStatusIdentifierBox, Priority.ALWAYS);
|
||||
gridPane.add(playbackStatusIdentifierBox, 2, row, 2, 1);
|
||||
|
||||
GridPane.setHalignment(getAudioSpectrumView(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getAudioSpectrumView(), Priority.ALWAYS);
|
||||
gridPane.add(getAudioSpectrumView(), 4, row, 1, 3);
|
||||
|
||||
row++;
|
||||
GridPane.setHgrow(getLockLabel(), Priority.NEVER);
|
||||
gridPane.add(getLockLabel(), 0, row);
|
||||
|
||||
Label toLabel = new Label("TO:");
|
||||
toLabel.setDisable(true);
|
||||
GridPane.setHalignment(toLabel, HPos.RIGHT);
|
||||
GridPane.setHgrow(toLabel, Priority.NEVER);
|
||||
gridPane.add(toLabel, 1, row);
|
||||
|
||||
GridPane.setHalignment(getToLabel(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getToLabel(), Priority.NEVER);
|
||||
gridPane.add(getToLabel(), 2, row);
|
||||
|
||||
GridPane.setHalignment(getToAliasLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getToAliasLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getToAliasLabel(), 3, row);
|
||||
|
||||
row++;
|
||||
|
||||
GridPane.setHgrow(getMuteLabel(), Priority.NEVER);
|
||||
gridPane.add(getMuteLabel(), 0, row);
|
||||
|
||||
Label fromLabel = new Label("FM:");
|
||||
fromLabel.setDisable(true);
|
||||
GridPane.setHalignment(fromLabel, HPos.RIGHT);
|
||||
GridPane.setHgrow(fromLabel, Priority.NEVER);
|
||||
gridPane.add(fromLabel, 1, row);
|
||||
|
||||
GridPane.setHalignment(getFromLabel(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getFromLabel(), Priority.NEVER);
|
||||
gridPane.add(getFromLabel(), 2, row);
|
||||
|
||||
GridPane.setHalignment(getFromAliasLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getFromAliasLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getFromAliasLabel(), 3, row);
|
||||
|
||||
row++;
|
||||
|
||||
Label channelLabel = new Label("CHAN:");
|
||||
channelLabel.setDisable(true);
|
||||
GridPane.setHalignment(channelLabel, HPos.RIGHT);
|
||||
GridPane.setHgrow(channelLabel, Priority.NEVER);
|
||||
gridPane.add(channelLabel, 0, row, 2, 1);
|
||||
|
||||
GridPane.setHalignment(getFrequencyLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getFrequencyLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getFrequencyLabel(), 2, row);
|
||||
|
||||
HBox systemSiteBox = new HBox();
|
||||
systemSiteBox.setSpacing(3);
|
||||
Label dashLabel = new Label("-");
|
||||
dashLabel.textProperty().bind(getSiteLabel().textProperty().map(s -> getSiteLabel()
|
||||
.textProperty().get() != null ? " - " : null));
|
||||
systemSiteBox.getChildren().addAll(getSystemLabel(), dashLabel, getSiteLabel());
|
||||
GridPane.setHalignment(systemSiteBox, HPos.LEFT);
|
||||
GridPane.setHgrow(systemSiteBox, Priority.ALWAYS);
|
||||
gridPane.add(systemSiteBox, 3, row, 2, 1);
|
||||
|
||||
ColumnConstraints cc0 = new ColumnConstraints();
|
||||
cc0.setPercentWidth(4);
|
||||
ColumnConstraints cc1 = new ColumnConstraints();
|
||||
cc1.setPercentWidth(4);
|
||||
ColumnConstraints cc2 = new ColumnConstraints();
|
||||
cc2.setPercentWidth(14);
|
||||
ColumnConstraints cc3 = new ColumnConstraints();
|
||||
cc3.setPercentWidth(28);
|
||||
ColumnConstraints cc4 = new ColumnConstraints();
|
||||
cc4.setPercentWidth(50);
|
||||
gridPane.getColumnConstraints().addAll(cc0, cc1, cc2, cc3, cc4);
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
private AudioSpectrumView getAudioSpectrumView()
|
||||
{
|
||||
if(mAudioSpectrumView == null)
|
||||
{
|
||||
mAudioSpectrumView = new AudioSpectrumView();
|
||||
}
|
||||
|
||||
return mAudioSpectrumView;
|
||||
}
|
||||
|
||||
private Label getFrequencyLabel()
|
||||
{
|
||||
if(mFrequencyLabel == null)
|
||||
{
|
||||
mFrequencyLabel = new Label();
|
||||
mFrequencyLabel.textProperty().bind(getController().frequencyProperty());
|
||||
}
|
||||
|
||||
return mFrequencyLabel;
|
||||
}
|
||||
|
||||
private Label getSiteLabel()
|
||||
{
|
||||
if(mSiteLabel == null)
|
||||
{
|
||||
mSiteLabel = new Label();
|
||||
mSiteLabel.textProperty().bind(getController().siteProperty());
|
||||
}
|
||||
|
||||
return mSiteLabel;
|
||||
}
|
||||
|
||||
private Label getNameLabel()
|
||||
{
|
||||
if(mNameLabel == null)
|
||||
{
|
||||
mNameLabel = new Label();
|
||||
mNameLabel.setDisable(true);
|
||||
mNameLabel.textProperty().bind(getController().nameProperty().concat(":"));
|
||||
}
|
||||
|
||||
return mNameLabel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Minimal audio playback view
|
||||
*/
|
||||
public class AudioPlaybackChannelViewMinimal extends AudioPlaybackChannelView
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param controller for audio playback that backs this view.
|
||||
*/
|
||||
public AudioPlaybackChannelViewMinimal(AudioPlaybackChannelController controller)
|
||||
{
|
||||
super(controller);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset width
|
||||
*/
|
||||
public void resetWidth()
|
||||
{
|
||||
//No-op
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setSpacing(3);
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, null, null)));
|
||||
setPadding(new Insets(0, 3, 0, 3));
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(3);
|
||||
|
||||
int row = 0;
|
||||
|
||||
GridPane.setHalignment(getMuteLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getMuteLabel(), Priority.NEVER);
|
||||
gridPane.add(getMuteLabel(), 0, row);
|
||||
|
||||
GridPane.setHalignment(getLockLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getLockLabel(), Priority.NEVER);
|
||||
gridPane.add(getLockLabel(), 1, row);
|
||||
|
||||
Label toLabel = new Label("TO:");
|
||||
toLabel.setDisable(true);
|
||||
GridPane.setHalignment(toLabel, HPos.RIGHT);
|
||||
GridPane.setHgrow(toLabel, Priority.NEVER);
|
||||
gridPane.add(toLabel, 2, row);
|
||||
|
||||
GridPane.setHalignment(getToLabel(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getToLabel(), Priority.NEVER);
|
||||
gridPane.add(getToLabel(), 3, row);
|
||||
|
||||
GridPane.setHalignment(getToAliasLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getToAliasLabel(), Priority.ALWAYS);
|
||||
getToAliasLabel().setMaxWidth(Double.MAX_VALUE);
|
||||
gridPane.add(getToAliasLabel(), 4, row);
|
||||
|
||||
GridPane.setHalignment(getPlaybackMode(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getPlaybackMode(), Priority.SOMETIMES);
|
||||
gridPane.add(getPlaybackMode(), 4, row);
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.BorderStroke;
|
||||
import javafx.scene.layout.BorderStrokeStyle;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Standard audio playback view
|
||||
*/
|
||||
public class AudioPlaybackChannelViewStandard extends AudioPlaybackChannelView
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param controller for audio playback that backs this view.
|
||||
*/
|
||||
public AudioPlaybackChannelViewStandard(AudioPlaybackChannelController controller)
|
||||
{
|
||||
super(controller);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset width
|
||||
*/
|
||||
public void resetWidth()
|
||||
{
|
||||
//No-op
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setSpacing(3);
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, null, null)));
|
||||
setPadding(new Insets(0, 3, 0, 3));
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(3);
|
||||
|
||||
int row = 0;
|
||||
|
||||
GridPane.setHalignment(getLockLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getLockLabel(), Priority.NEVER);
|
||||
gridPane.add(getLockLabel(), 0, row);
|
||||
|
||||
Label toLabel = new Label("TO:");
|
||||
toLabel.setDisable(true);
|
||||
GridPane.setHalignment(toLabel, HPos.RIGHT);
|
||||
GridPane.setHgrow(toLabel, Priority.NEVER);
|
||||
gridPane.add(toLabel, 1, row);
|
||||
|
||||
GridPane.setHalignment(getToLabel(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getToLabel(), Priority.NEVER);
|
||||
gridPane.add(getToLabel(), 2, row);
|
||||
|
||||
GridPane.setHalignment(getToAliasLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getToAliasLabel(), Priority.ALWAYS);
|
||||
getToAliasLabel().setMaxWidth(Double.MAX_VALUE);
|
||||
gridPane.add(getToAliasLabel(), 3, row);
|
||||
|
||||
GridPane.setHalignment(getPlaybackMode(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getPlaybackMode(), Priority.SOMETIMES);
|
||||
gridPane.add(getPlaybackMode(), 4, row);
|
||||
|
||||
row++;
|
||||
|
||||
GridPane.setHalignment(getMuteLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getMuteLabel(), Priority.NEVER);
|
||||
gridPane.add(getMuteLabel(), 0, row);
|
||||
|
||||
Label fromLabel = new Label("FM:");
|
||||
fromLabel.setDisable(true);
|
||||
GridPane.setHalignment(fromLabel, HPos.RIGHT);
|
||||
GridPane.setHgrow(fromLabel, Priority.NEVER);
|
||||
gridPane.add(fromLabel, 1, row);
|
||||
|
||||
GridPane.setHalignment(getFromLabel(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getFromLabel(), Priority.NEVER);
|
||||
gridPane.add(getFromLabel(), 2, row);
|
||||
|
||||
GridPane.setHalignment(getFromAliasLabel(), HPos.LEFT);
|
||||
GridPane.setHgrow(getFromAliasLabel(), Priority.ALWAYS);
|
||||
gridPane.add(getFromAliasLabel(), 3, row);
|
||||
|
||||
GridPane.setHalignment(getMediaPlayerStatus(), HPos.RIGHT);
|
||||
GridPane.setHgrow(getMediaPlayerStatus(), Priority.SOMETIMES);
|
||||
gridPane.add(getMediaPlayerStatus(), 4, row);
|
||||
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
import io.github.dsheirer.preference.IPreferenceUpdateListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
/**
|
||||
* Audio playback view for one or more audio playback channel views and controllers.
|
||||
*/
|
||||
public class AudioPlaybackChannelsView extends HBox implements IPreferenceUpdateListener
|
||||
{
|
||||
@Resource
|
||||
private AudioPlaybackController mAudioPlaybackController;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
private List<AudioPlaybackChannelView> mAudioPlaybackChannelViews = new ArrayList<>();
|
||||
private GridPane mGridPane;
|
||||
private PlaybackView mPlaybackView;
|
||||
private IPanelRevalidationRequestListener mListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public AudioPlaybackChannelsView()
|
||||
{
|
||||
setPadding(new Insets(0, 3, 0, 2));
|
||||
setSpacing(3);
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
//Force the audio spectrum view's image view to (temporarily) shrink to 1 so that we can resize dynamically.
|
||||
widthProperty().addListener((observable, oldValue, newValue) -> resetWidth());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive requests from this view for Swing revalidation
|
||||
* @param listener to receive request.
|
||||
*/
|
||||
public void setPanelRevalidationRequestListener(IPanelRevalidationRequestListener listener)
|
||||
{
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements preference type update listener to be notified if the user changes the playback view preference.
|
||||
* @param preferenceType that was updated
|
||||
*/
|
||||
@Override
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
{
|
||||
PlaybackView preferred = mUserPreferences.getPlaybackPreference().getPlaybackView();
|
||||
|
||||
if(mPlaybackView != preferred)
|
||||
{
|
||||
mPlaybackView = preferred;
|
||||
updatePlaybackViews();
|
||||
|
||||
//Since this view is embedded in a JFXPanel, the parent has to nullify the JFXPanel's scene and reset it.
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.revalidatePanel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the audio spectrum view width during resizing.
|
||||
*/
|
||||
public void resetWidth()
|
||||
{
|
||||
for(AudioPlaybackChannelView view: mAudioPlaybackChannelViews)
|
||||
{
|
||||
view.resetWidth();
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePlaybackViews()
|
||||
{
|
||||
if(!mAudioPlaybackChannelViews.isEmpty())
|
||||
{
|
||||
for(AudioPlaybackChannelView view: mAudioPlaybackChannelViews)
|
||||
{
|
||||
getGridPane().getChildren().remove(view);
|
||||
}
|
||||
|
||||
mAudioPlaybackChannelViews.clear();
|
||||
}
|
||||
|
||||
AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
|
||||
AudioPlaybackChannelView leftView = switch(mPlaybackView)
|
||||
{
|
||||
case MINIMAL -> new AudioPlaybackChannelViewMinimal(left);
|
||||
case STANDARD -> new AudioPlaybackChannelViewStandard(left);
|
||||
default -> new AudioPlaybackChannelViewComplete(left);
|
||||
};
|
||||
|
||||
getGridPane().add(leftView, 0, 0);
|
||||
mAudioPlaybackChannelViews.add(leftView);
|
||||
|
||||
AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
|
||||
AudioPlaybackChannelView rightView = switch(mPlaybackView)
|
||||
{
|
||||
case MINIMAL -> new AudioPlaybackChannelViewMinimal(right);
|
||||
case STANDARD -> new AudioPlaybackChannelViewStandard(right);
|
||||
default -> new AudioPlaybackChannelViewComplete(right);
|
||||
};
|
||||
getGridPane().add(rightView, 1, 0);
|
||||
mAudioPlaybackChannelViews.add(rightView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Post instantiation/startup steps.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
mPlaybackView = mUserPreferences.getPlaybackPreference().getPlaybackView();
|
||||
|
||||
//Register for notifications of user changes to the audio playback view style
|
||||
mUserPreferences.addUpdateListener(this);
|
||||
|
||||
HBox.setHgrow(getGridPane(), Priority.ALWAYS);
|
||||
updatePlaybackViews();
|
||||
getChildren().add(getGridPane());
|
||||
}
|
||||
|
||||
private GridPane getGridPane()
|
||||
{
|
||||
if(mGridPane == null)
|
||||
{
|
||||
mGridPane = new GridPane();
|
||||
mGridPane.setMaxWidth(Double.MAX_VALUE);
|
||||
mGridPane.setHgap(3);
|
||||
|
||||
//Add two columns and give them each 50% of the width
|
||||
ColumnConstraints ccLeft = new ColumnConstraints();
|
||||
ccLeft.setPercentWidth(100.0 / 2);
|
||||
ccLeft.setHgrow(Priority.ALWAYS);
|
||||
mGridPane.getColumnConstraints().add(ccLeft);
|
||||
|
||||
ColumnConstraints ccRight = new ColumnConstraints();
|
||||
ccRight.setPercentWidth(100.0 / 2);
|
||||
ccRight.setHgrow(Priority.ALWAYS);
|
||||
mGridPane.getColumnConstraints().add(ccRight);
|
||||
}
|
||||
|
||||
return mGridPane;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.audio.call.Call;
|
||||
import io.github.dsheirer.audio.call.ICallEventListener;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Controller for application audio playback channels
|
||||
*/
|
||||
@Component("audioPlaybackController")
|
||||
public class AudioPlaybackController implements ICallEventListener, IAudioPlaybackStatusListener
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AudioPlaybackController.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOG);
|
||||
private static final double AUDIO_BALANCE_LEFT = -1.0;
|
||||
private static final double AUDIO_BALANCE_RIGHT = 1.0;
|
||||
|
||||
private Map<MixerChannel, AudioPlaybackChannelController> mControllerMap = new HashMap<>();
|
||||
private LinkedTransferQueue<Call> mNewCallQueue = new LinkedTransferQueue<>();
|
||||
private List<Call> mCalls = new ArrayList<>();
|
||||
private LinkedTransferQueue<PlaybackRequest> mRequestQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledFuture<?> mQueueProcessorFuture;
|
||||
private int mMaxCallQueueSize = 25;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public AudioPlaybackController()
|
||||
{
|
||||
mControllerMap.put(MixerChannel.LEFT, new AudioPlaybackChannelController(MixerChannel.LEFT, AUDIO_BALANCE_LEFT));
|
||||
mControllerMap.put(MixerChannel.RIGHT, new AudioPlaybackChannelController(MixerChannel.RIGHT, AUDIO_BALANCE_RIGHT));
|
||||
|
||||
//Register as audio playback status listener
|
||||
for(AudioPlaybackChannelController controller: mControllerMap.values())
|
||||
{
|
||||
controller.add(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post instantiation/startup steps.
|
||||
*/
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
if(mQueueProcessorFuture == null)
|
||||
{
|
||||
//Run at 250ms or four times a second
|
||||
mQueueProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new QueueProcessor(),
|
||||
0, 250, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-destruction or shutdown steps
|
||||
*/
|
||||
@PreDestroy
|
||||
public void preDestroy()
|
||||
{
|
||||
if(mQueueProcessorFuture != null)
|
||||
{
|
||||
mQueueProcessorFuture.cancel(true);
|
||||
}
|
||||
|
||||
mRequestQueue.clear();
|
||||
mNewCallQueue.clear();
|
||||
mCalls.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process request and call queues and manage playback state of both channels. This method is intended to be
|
||||
* invoked by a (single) threaded processor.
|
||||
*/
|
||||
private void process()
|
||||
{
|
||||
//Add new calls to the calls list.
|
||||
mNewCallQueue.drainTo(mCalls);
|
||||
|
||||
//Process the request queue
|
||||
PlaybackRequest request = mRequestQueue.poll();
|
||||
|
||||
while(request != null)
|
||||
{
|
||||
AudioPlaybackChannelController controller = mControllerMap.get(request.mixerChannel);
|
||||
|
||||
switch(request.playbackMode)
|
||||
{
|
||||
case AUTO -> controller.auto();
|
||||
case LOCKED -> controller.lockAutoTo(request.identifier, request.system);
|
||||
case MUTE -> controller.mute();
|
||||
case REPLAY ->
|
||||
{
|
||||
controller.replay(request.call);
|
||||
mCalls.remove(request.call);
|
||||
}
|
||||
}
|
||||
|
||||
request = mRequestQueue.poll();
|
||||
}
|
||||
|
||||
//Remove duplicate calls
|
||||
mCalls.removeIf(call -> call.isDuplicate());
|
||||
|
||||
if(mCalls.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AudioPlaybackChannelController left = mControllerMap.get(MixerChannel.LEFT);
|
||||
AudioPlaybackChannelController right = mControllerMap.get(MixerChannel.LEFT);
|
||||
|
||||
//Reduce calls queue size to max size but don't remove any calls tagged for locked replay.
|
||||
int excessCalls = mCalls.size() - mMaxCallQueueSize;
|
||||
if(excessCalls > 0)
|
||||
{
|
||||
Iterator<Call> it = mCalls.iterator();
|
||||
|
||||
while(it.hasNext() && excessCalls > 0)
|
||||
{
|
||||
Call call = it.next();
|
||||
if(!(left.isLockedFor(call) || right.isLockedFor(call)))
|
||||
{
|
||||
it.remove();
|
||||
excessCalls--;
|
||||
System.out.println("Removed excess call TO:" + call.getToId() + " FM:" + call.getFromId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Process left audio channel if it is currently empty and set for auto replay.
|
||||
if(left.isAvailableForAutoPlay())
|
||||
{
|
||||
List<Call> leftCalls = new ArrayList<>(mCalls);
|
||||
|
||||
//Remove any calls locked to the right channel
|
||||
if(right.isLocked())
|
||||
{
|
||||
Iterator<Call> it = leftCalls.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
if(right.isLockedFor(it.next()))
|
||||
{
|
||||
it.remove();;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(left.isLocked())
|
||||
{
|
||||
for(int x = 0; x < leftCalls.size(); x++)
|
||||
{
|
||||
Call call = leftCalls.get(x);
|
||||
if(left.isLockedFor(call))
|
||||
{
|
||||
mCalls.remove(call);
|
||||
left.play(call);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Call call = leftCalls.get(0);
|
||||
mCalls.remove(call);
|
||||
System.out.println("Playing call to: " + call.getToId() + " fm:" + call.getFromId());
|
||||
left.play(call);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if(mCalls.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
//Process right audio channel if it is currently empty and set for auto replay.
|
||||
if(right.isAvailableForAutoPlay())
|
||||
{
|
||||
List<Call> rightCalls = new ArrayList<>(mCalls);
|
||||
|
||||
//Remove any calls locked to the left channel
|
||||
if(left.isLocked())
|
||||
{
|
||||
Iterator<Call> it = rightCalls.iterator();
|
||||
while(it.hasNext())
|
||||
{
|
||||
if(left.isLockedFor(it.next()))
|
||||
{
|
||||
it.remove();;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(right.isLocked())
|
||||
{
|
||||
for(int x = 0; x < rightCalls.size(); x++)
|
||||
{
|
||||
Call call = rightCalls.get(x);
|
||||
if(right.isLockedFor(call))
|
||||
{
|
||||
mCalls.remove(call);
|
||||
right.play(call);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Call call = rightCalls.get(0);
|
||||
mCalls.remove(call);
|
||||
right.play(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request replay of the specified call.
|
||||
* @param mixerChannel for the replay
|
||||
* @param call to replay.
|
||||
*/
|
||||
public void replay(MixerChannel mixerChannel, Call call)
|
||||
{
|
||||
mRequestQueue.add(new PlaybackRequest(mixerChannel, PlaybackMode.REPLAY, call, null, null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Request auto-playback mode and optionally specifies a locked identifier.
|
||||
* @param mixerChannel for the auto mode.
|
||||
* @param identifier (optional) for locked replay.
|
||||
* @param system (optional) for locked replay.
|
||||
*/
|
||||
public void auto(MixerChannel mixerChannel, String identifier, String system)
|
||||
{
|
||||
mRequestQueue.add(new PlaybackRequest(mixerChannel,
|
||||
(identifier == null ? PlaybackMode.AUTO : PlaybackMode.LOCKED), null, identifier, system));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the specified mixer channel.
|
||||
* @param mixerChannel to mute.
|
||||
*/
|
||||
public void mute(MixerChannel mixerChannel)
|
||||
{
|
||||
mRequestQueue.add(new PlaybackRequest(mixerChannel, PlaybackMode.MUTE, null, null, null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus,
|
||||
MediaPlayer.Status currentStatus)
|
||||
{
|
||||
//TODO: do I need this?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endOfMedia(AudioPlaybackChannelController controller)
|
||||
{
|
||||
//TODO: do I need this?
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary audio controller for manual playback.
|
||||
*/
|
||||
public AudioPlaybackChannelController getPrimaryController()
|
||||
{
|
||||
return mControllerMap.get(MixerChannel.LEFT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the channel controller associated with the mixer channel.
|
||||
* @return controller
|
||||
*/
|
||||
public AudioPlaybackChannelController getChannelController(MixerChannel mixerChannel)
|
||||
{
|
||||
return mControllerMap.get(mixerChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void added(Call call)
|
||||
{
|
||||
mNewCallQueue.add(call);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updated(Call call) {}
|
||||
@Override
|
||||
public void completed(Call call, AudioSegment audioSegment) {}
|
||||
@Override
|
||||
public void deleted(Call call) {}
|
||||
|
||||
/**
|
||||
* Playback request
|
||||
* @param mixerChannel for the request (LEFT or RIGHT)
|
||||
* @param playbackMode to apply
|
||||
* @param call (optional) to replay
|
||||
* @param identifier (optional) for locked replay.
|
||||
* @param system (optional) for locked replay.
|
||||
*/
|
||||
private record PlaybackRequest(MixerChannel mixerChannel, PlaybackMode playbackMode, Call call,
|
||||
String identifier, String system)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable queue processor.
|
||||
*/
|
||||
private class QueueProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
process();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error(t.getMessage(), 3, "Error while processing audio queue", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.playbackfx;
|
||||
|
||||
import java.util.Arrays;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.PixelFormat;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.media.AudioSpectrumListener;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
|
||||
/**
|
||||
* Audio spectrum view provides visual display of the frequency content of the replay audio.
|
||||
*/
|
||||
public class AudioSpectrumView extends VBox implements AudioSpectrumListener
|
||||
{
|
||||
private ObjectProperty<MediaPlayer> mMediaPlayer = new SimpleObjectProperty<>();
|
||||
private static final int AUDIO_SPECTRUM_FFT_BANDS = 64;
|
||||
private static final int HEIGHT = 64; //64-FFT bins plotted with one pixel per bin
|
||||
private Color mColor = Color.YELLOW;
|
||||
private PixelFormat mPixelFormat;
|
||||
private WritableImage mWritableImage;
|
||||
private ImageView mImageView;
|
||||
private int mColorIntensity = 10;
|
||||
private int mTimeAxisOffset = 0;
|
||||
private int mPreviousPlaybackLine;
|
||||
private byte[] mPlaybackLine;
|
||||
private byte[] mClearedPlaybackLine;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public AudioSpectrumView()
|
||||
{
|
||||
setHeight(64);
|
||||
setBackground(Background.fill(Color.BLACK));
|
||||
updateColor();
|
||||
getChildren().add(getImageView());
|
||||
|
||||
//Monitor the width property to dynamically resize the writable image
|
||||
widthProperty().addListener((observable, oldValue, newWidth) -> {
|
||||
mWritableImage = new WritableImage(newWidth.intValue(), HEIGHT);
|
||||
mImageView.setImage(mWritableImage);
|
||||
});
|
||||
|
||||
//Allow user to click within the spectrum area to seek playback to the click location
|
||||
setOnMouseClicked(event -> {
|
||||
double requestedPlayback = (mTimeAxisOffset + event.getX()) / 20.0;
|
||||
MediaPlayer mediaPlayer = mediaPlayerProperty().get();
|
||||
if(mediaPlayer != null)
|
||||
{
|
||||
mediaPlayer.seek(Duration.seconds(requestedPlayback));
|
||||
}
|
||||
});
|
||||
|
||||
//Monitor the media player to register and deregister from audio spectrum results and DFT bin sizing.
|
||||
mediaPlayerProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(oldValue != null)
|
||||
{
|
||||
oldValue.setAudioSpectrumListener(null);
|
||||
reset();
|
||||
}
|
||||
|
||||
if(newValue != null)
|
||||
{
|
||||
//Register to receive audio spectrum FFT bins
|
||||
newValue.setAudioSpectrumListener(AudioSpectrumView.this::spectrumDataUpdate);
|
||||
//Change the FFT bin setting from default 128 bins
|
||||
newValue.setAudioSpectrumNumBands(AUDIO_SPECTRUM_FFT_BANDS);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the width when the parent container is resizing. Sets the width to 1 so that when new FFT bins come
|
||||
* in, the image can be updated to the current width.
|
||||
*/
|
||||
public void resetWidth()
|
||||
{
|
||||
mWritableImage = new WritableImage(1, HEIGHT);
|
||||
mImageView.setImage(mWritableImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Media player property. Bind this property to a media player so that it can register to receive audio spectrum
|
||||
* data and control the FFT bins setting.
|
||||
*/
|
||||
public ObjectProperty<MediaPlayer> mediaPlayerProperty()
|
||||
{
|
||||
return mMediaPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the spectral view
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mWritableImage = null;
|
||||
getImageView().setImage(getWritableImage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Image view for displaying the writable image.
|
||||
*/
|
||||
private ImageView getImageView()
|
||||
{
|
||||
if(mImageView == null)
|
||||
{
|
||||
mImageView = new ImageView(getWritableImage());
|
||||
}
|
||||
|
||||
return mImageView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writable image for plotting FFT spectrum results.
|
||||
*/
|
||||
private WritableImage getWritableImage()
|
||||
{
|
||||
if(mWritableImage == null)
|
||||
{
|
||||
mWritableImage = new WritableImage(1, HEIGHT); //Initial width ... gets reset once visible.
|
||||
}
|
||||
|
||||
return mWritableImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the pixel format color array when the base color changes.
|
||||
*/
|
||||
private void updateColor()
|
||||
{
|
||||
int[] argbColors = new int[256];
|
||||
int red = (int)(mColor.getRed() * 255);
|
||||
int green = (int)(mColor.getGreen() * 255);
|
||||
int blue = (int)(mColor.getBlue() * 255);
|
||||
int rgb = ((red & 0xFF) << 16) + ((green & 0xFF) << 8) + (blue & 0xFF);
|
||||
for(int alpha = 0; alpha < 256; alpha++)
|
||||
{
|
||||
int tempAlpha = alpha * mColorIntensity;
|
||||
if(tempAlpha > 255)
|
||||
{
|
||||
tempAlpha = 255;
|
||||
}
|
||||
argbColors[alpha] = ((tempAlpha & 0xFF) << 24) + rgb;
|
||||
}
|
||||
|
||||
mPixelFormat = PixelFormat.createByteIndexedInstance(argbColors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the audio spectrum listener interface to receive FFT bin results during audio playback.
|
||||
*
|
||||
* @param timestamp timestamp of the event in seconds.
|
||||
* @param duration duration for which the spectrum was computed in seconds.
|
||||
* @param magnitudes array containing the non-positive spectrum magnitude in decibels
|
||||
* (dB) for each band.
|
||||
* The size of the array equals the number of bands and should be considered
|
||||
* to be read-only.
|
||||
* @param phases array containing the phase in the range
|
||||
* [<code>Math.PI</code>, <code>Math.PI</code>] for each band.
|
||||
* The size of the array equals the number of bands and should be considered
|
||||
* to be read-only.
|
||||
*/
|
||||
@Override
|
||||
public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases)
|
||||
{
|
||||
if(mWritableImage.getWidth() != getWidth())
|
||||
{
|
||||
mWritableImage = new WritableImage((int)getWidth(), HEIGHT);
|
||||
mImageView.setImage(mWritableImage);
|
||||
}
|
||||
|
||||
if(mPlaybackLine == null || mPlaybackLine.length != magnitudes.length)
|
||||
{
|
||||
mPlaybackLine = new byte[magnitudes.length];
|
||||
Arrays.fill(mPlaybackLine, (byte)(0xFF & 255));
|
||||
mClearedPlaybackLine = new byte[magnitudes.length]; //Default to all zeros
|
||||
}
|
||||
|
||||
int timeAxis = (int)Math.floor(timestamp * 10) * 2;
|
||||
|
||||
//Detect when time axis offset is non-zero and playback resets to zero-ish so we can reset.
|
||||
if(timeAxis < mTimeAxisOffset)
|
||||
{
|
||||
mTimeAxisOffset = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
timeAxis -= mTimeAxisOffset;
|
||||
}
|
||||
|
||||
|
||||
//If the playback time exceeds the width of the display, shift the displayed content to the left by 25% and
|
||||
//continue rendering. This will make for an animated left scrolling audio playback.
|
||||
if(timeAxis > (getWritableImage().getWidth() - 3))
|
||||
{
|
||||
int offset = (int)(getWidth() / 4);
|
||||
|
||||
//Shift the current image to the left by the offset
|
||||
mWritableImage.getPixelWriter().setPixels(0, 0, (int)(mWritableImage.getWidth() - offset),
|
||||
HEIGHT, mWritableImage.getPixelReader(), offset, 0);
|
||||
|
||||
mWritableImage.getPixelWriter().setPixels((int)mWritableImage.getWidth() - offset, 0, offset, HEIGHT,
|
||||
mPixelFormat, new byte[offset * HEIGHT], 0, 1);
|
||||
|
||||
mTimeAxisOffset += offset;
|
||||
timeAxis -= offset;
|
||||
}
|
||||
|
||||
final byte[] values = new byte[magnitudes.length];
|
||||
for(int x = 0; x < magnitudes.length; x++)
|
||||
{
|
||||
float pixelValues = magnitudes[x];
|
||||
|
||||
if(pixelValues <= -60f)
|
||||
{
|
||||
values[magnitudes.length - x - 1] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
values[magnitudes.length - x - 1] = (byte)((int)((60.0f + pixelValues) / 60.0f * 256.0) & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
//Clear the previous playback line
|
||||
if(mPreviousPlaybackLine < getWritableImage().getWidth())
|
||||
{
|
||||
getWritableImage().getPixelWriter().setPixels(mPreviousPlaybackLine, 0, 1, mClearedPlaybackLine.length,
|
||||
mPixelFormat, mClearedPlaybackLine, 0, 1);
|
||||
}
|
||||
getWritableImage().getPixelWriter().setPixels(timeAxis++, 0, 1, values.length, mPixelFormat, values, 0, 1);
|
||||
getWritableImage().getPixelWriter().setPixels(timeAxis++, 0, 1, values.length, mPixelFormat, values, 0, 1);
|
||||
getWritableImage().getPixelWriter().setPixels(timeAxis, 0, 1, mPlaybackLine.length, mPixelFormat, mPlaybackLine, 0, 1);
|
||||
mPreviousPlaybackLine = timeAxis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.playbackfx;
|
||||
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
|
||||
/**
|
||||
* Listener interface for audio playback status events.
|
||||
*/
|
||||
public interface IAudioPlaybackStatusListener
|
||||
{
|
||||
/**
|
||||
* Notify the listener that the status of the media player has changed.
|
||||
* @param controller for the media player
|
||||
* @param previousStatus of the media player.
|
||||
* @param currentStatus of the media player.
|
||||
*/
|
||||
void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus, MediaPlayer.Status currentStatus);
|
||||
|
||||
/**
|
||||
* Notifies the listener that current playback encountered the end of the audio media.
|
||||
*/
|
||||
void endOfMedia(AudioPlaybackChannelController controller);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
/**
|
||||
* Interface to receive request for a Swing UI revaliation request from within JavaFX.
|
||||
*/
|
||||
public interface IPanelRevalidationRequestListener
|
||||
{
|
||||
void revalidatePanel();
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.audio.playbackfx;
|
||||
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* Playback modes for left/right audio channels.
|
||||
*/
|
||||
public enum PlaybackMode
|
||||
{
|
||||
AUTO,
|
||||
REPLAY,
|
||||
LOCKED,
|
||||
MUTE;
|
||||
|
||||
private static final EnumSet<PlaybackMode> AUTO_PLAY_MODES = EnumSet.of(AUTO, LOCKED);
|
||||
|
||||
/**
|
||||
* Indicates if the playback mode is an autoplay mode (AUTO or LOCKED).
|
||||
* @return true if an autoplay mode.
|
||||
*/
|
||||
public boolean isAutoPlayable()
|
||||
{
|
||||
return AUTO_PLAY_MODES.contains(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.audio.playbackfx;
|
||||
|
||||
/**
|
||||
* Audio playback view enumeration
|
||||
*/
|
||||
public enum PlaybackView
|
||||
{
|
||||
RICH("Rich"),
|
||||
STANDARD("Standard"),
|
||||
MINIMAL("Minimal");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param label to use for display purposes
|
||||
*/
|
||||
PlaybackView(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -14,8 +14,8 @@
|
||||
*
|
||||
* 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.channel.details;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderState;
|
||||
@@ -23,6 +23,11 @@ import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.module.ProcessingChain;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JButton;
|
||||
@@ -31,29 +36,25 @@ import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTextArea;
|
||||
import javax.swing.text.DefaultCaret;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
public class ChannelDetailPanel extends JPanel implements Listener<ProcessingChain>
|
||||
{
|
||||
private static final String EMPTY_DETAILS = "Please select a channel to view details";
|
||||
|
||||
@Resource
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
|
||||
private JLabel mSystemLabel;
|
||||
private JLabel mSiteLabel;
|
||||
private JLabel mNameLabel;
|
||||
private JTextArea mDetailTextPane;
|
||||
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
private ProcessingChain mProcessingChain;
|
||||
|
||||
public ChannelDetailPanel(ChannelProcessingManager channelProcessingManager)
|
||||
public ChannelDetailPanel()
|
||||
{
|
||||
mChannelProcessingManager = channelProcessingManager;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[]0[grow,fill]"));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -35,11 +35,10 @@ import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier
|
||||
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Channel metadata containing details about the channel configuration, decoder state and current
|
||||
@@ -66,17 +65,27 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
private AliasModel mAliasModel;
|
||||
private AliasList mAliasList;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param aliasModel for alias lookup
|
||||
* @param timeslot for this metadata
|
||||
*/
|
||||
public ChannelMetadata(AliasModel aliasModel, Integer timeslot)
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
mTimeslot = timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance with timeslot (default = 0)
|
||||
* @param aliasModel for alias lookup
|
||||
*/
|
||||
public ChannelMetadata(AliasModel aliasModel)
|
||||
{
|
||||
this(aliasModel, null);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
|
||||
public Integer getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
|
||||
@@ -1,46 +1,48 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.channel.metadata;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.preference.IPreferenceUpdateListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.EventQueue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
|
||||
@Component("channelMetadataModel")
|
||||
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener, IPreferenceUpdateListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataModel.class);
|
||||
|
||||
@@ -59,17 +61,30 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
|
||||
private List<ChannelMetadata> mChannelMetadata = new ArrayList();
|
||||
private Map<ChannelMetadata,Channel> mMetadataChannelMap = new HashMap();
|
||||
private Listener<ChannelAndMetadata> mChannelAddListener;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
public ChannelMetadataModel()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
mUserPreferences.addUpdateListener(this);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void preDestroy()
|
||||
{
|
||||
mUserPreferences.removeUpdateListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives preference update notifications via the event bus
|
||||
* @param preferenceType that was updated
|
||||
*/
|
||||
@Subscribe
|
||||
@Override
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
if(preferenceType == PreferenceType.TALKGROUP_FORMAT)
|
||||
@@ -84,11 +99,6 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup model row for the specified channel metadata
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -23,7 +23,6 @@ import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.channel.state.State;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelModel;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.playlist.channel.ViewChannelRequest;
|
||||
@@ -32,12 +31,21 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
|
||||
import io.github.dsheirer.module.ProcessingChain;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
|
||||
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -52,23 +60,19 @@ import javax.swing.SwingConstants;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChannelMetadataPanel extends JPanel implements ListSelectionListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class);
|
||||
|
||||
private static final String TABLE_PREFERENCE_KEY = "channel.metadata.panel";
|
||||
private ChannelModel mChannelModel;
|
||||
@Resource
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
@Resource
|
||||
private IconModel mIconModel;
|
||||
@Resource
|
||||
private ChannelMetadataModel mChannelMetadataModel;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
private JTable mTable;
|
||||
private Broadcaster<ProcessingChain> mSelectedProcessingChainBroadcaster = new Broadcaster<>();
|
||||
@@ -80,25 +84,20 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
/**
|
||||
* Table view for currently decoding channel metadata
|
||||
*/
|
||||
public ChannelMetadataPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences)
|
||||
public ChannelMetadataPanel()
|
||||
{
|
||||
mChannelModel = playlistManager.getChannelModel();
|
||||
mChannelProcessingManager = playlistManager.getChannelProcessingManager();
|
||||
mIconModel = iconModel;
|
||||
mUserPreferences = userPreferences;
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the panel
|
||||
*/
|
||||
private void init()
|
||||
@PostConstruct
|
||||
private void postConstruct()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||
|
||||
mTable = new JTable(mChannelProcessingManager.getChannelMetadataModel());
|
||||
mChannelProcessingManager.getChannelMetadataModel().setChannelAddListener(new ChannelAddListener());
|
||||
|
||||
mTable = new JTable(mChannelMetadataModel);
|
||||
mChannelMetadataModel.setChannelAddListener(new ChannelAddListener());
|
||||
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class);
|
||||
renderer.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
@@ -119,7 +118,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
.setCellRenderer(new FrequencyCellRenderer());
|
||||
|
||||
//Add a table column width monitor to store/restore column widths
|
||||
mTableColumnMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
|
||||
mTableColumnMonitor = new JTableColumnWidthMonitor(mTable, TABLE_PREFERENCE_KEY);
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(mTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
|
||||
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
|
||||
@@ -167,14 +166,11 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
int selectedModelRow = mTable.convertRowIndexToModel(selectedViewRow);
|
||||
|
||||
ChannelMetadata selectedMetadata = mChannelProcessingManager.getChannelMetadataModel()
|
||||
.getChannelMetadata(selectedModelRow);
|
||||
ChannelMetadata selectedMetadata = mChannelMetadataModel.getChannelMetadata(selectedModelRow);
|
||||
|
||||
if(selectedMetadata != null)
|
||||
{
|
||||
mUserSelectedChannel = mChannelProcessingManager.getChannelMetadataModel()
|
||||
.getChannelFromMetadata(selectedMetadata);
|
||||
|
||||
mUserSelectedChannel = mChannelMetadataModel.getChannelFromMetadata(selectedMetadata);
|
||||
processingChain = mChannelProcessingManager.getProcessingChain(mUserSelectedChannel);
|
||||
}
|
||||
}
|
||||
@@ -404,12 +400,11 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
|
||||
if(modelRowIndex >= 0)
|
||||
{
|
||||
ChannelMetadata metadata = mChannelProcessingManager.getChannelMetadataModel().getChannelMetadata(modelRowIndex);
|
||||
ChannelMetadata metadata = mChannelMetadataModel.getChannelMetadata(modelRowIndex);
|
||||
|
||||
if(metadata != null)
|
||||
{
|
||||
Channel channel = mChannelProcessingManager.getChannelMetadataModel()
|
||||
.getChannelFromMetadata(metadata);
|
||||
Channel channel = mChannelMetadataModel.getChannelFromMetadata(metadata);
|
||||
|
||||
if(channel != null)
|
||||
{
|
||||
@@ -455,7 +450,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
|
||||
if(metadata.size() > 0)
|
||||
{
|
||||
int modelRow = mChannelProcessingManager.getChannelMetadataModel().getRow(metadata.get(0));
|
||||
int modelRow = mChannelMetadataModel.getRow(metadata.get(0));
|
||||
|
||||
if(modelRow >= 0)
|
||||
{
|
||||
|
||||
@@ -22,12 +22,10 @@ import com.jidesoft.swing.JideSplitPane;
|
||||
import com.jidesoft.swing.JideTabbedPane;
|
||||
import io.github.dsheirer.channel.details.ChannelDetailPanel;
|
||||
import io.github.dsheirer.gui.power.ChannelPowerPanel;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
|
||||
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.Color;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
@@ -38,30 +36,43 @@ import javax.swing.JPanel;
|
||||
*/
|
||||
public class NowPlayingPanel extends JPanel
|
||||
{
|
||||
private final ChannelMetadataPanel mChannelMetadataPanel;
|
||||
private final ChannelDetailPanel mChannelDetailPanel;
|
||||
private final DecodeEventPanel mDecodeEventPanel;
|
||||
private final MessageActivityPanel mMessageActivityPanel;
|
||||
private final ChannelPowerPanel mChannelPowerPanel;
|
||||
@Resource
|
||||
private ChannelDetailPanel mChannelDetailPanel;
|
||||
@Resource
|
||||
private ChannelMetadataPanel mChannelMetadataPanel;
|
||||
@Resource
|
||||
private ChannelPowerPanel mChannelPowerPanel;
|
||||
@Resource
|
||||
private DecodeEventPanel mDecodeEventPanel;
|
||||
private final MessageActivityPanel mMessageActivityPanel = new MessageActivityPanel();
|
||||
private JideTabbedPane mTabbedPane;
|
||||
private JideSplitPane mSplitPane;
|
||||
private boolean mDetailTabsVisible;
|
||||
private boolean mDetailTabsVisible = true;
|
||||
|
||||
/**
|
||||
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||
* messages, events, and spectral view.
|
||||
*/
|
||||
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
||||
SettingsManager settingsManager, boolean detailTabsVisible)
|
||||
public NowPlayingPanel()
|
||||
{
|
||||
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
||||
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
||||
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
||||
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
||||
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
||||
mDetailTabsVisible = detailTabsVisible;
|
||||
}
|
||||
|
||||
init();
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||
getSplitPane().add(mChannelMetadataPanel);
|
||||
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
|
||||
add(getSplitPane());
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,21 +131,4 @@ public class NowPlayingPanel extends JPanel
|
||||
|
||||
return mSplitPane;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||
getSplitPane().add(mChannelMetadataPanel);
|
||||
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
|
||||
add(getSplitPane());
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -44,13 +44,12 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
|
||||
@@ -91,10 +90,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param channel configuration
|
||||
* @param aliasModel for channel metadata and identifiers
|
||||
* @param timeslots array of timeslot numbers to use
|
||||
*/
|
||||
public MultiChannelState(Channel channel, AliasModel aliasModel, int[] timeslots)
|
||||
public MultiChannelState(AliasModel aliasModel, Channel channel, int[] timeslots)
|
||||
{
|
||||
super(channel);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -46,11 +46,10 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Channel state tracks the overall state of all processing modules and decoders configured for the channel and
|
||||
@@ -96,7 +95,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
|
||||
private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache();
|
||||
|
||||
public SingleChannelState(Channel channel, AliasModel aliasModel)
|
||||
public SingleChannelState(AliasModel aliasModel, Channel channel)
|
||||
{
|
||||
super(channel);
|
||||
mChannelMetadata = new ChannelMetadata(aliasModel);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -19,21 +19,23 @@
|
||||
package io.github.dsheirer.controller;
|
||||
|
||||
import com.jidesoft.swing.JideTabbedPane;
|
||||
import io.github.dsheirer.audio.call.CallView;
|
||||
import io.github.dsheirer.audio.playback.AudioPanel;
|
||||
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
|
||||
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelsView;
|
||||
import io.github.dsheirer.audio.playbackfx.IPanelRevalidationRequestListener;
|
||||
import io.github.dsheirer.channel.metadata.NowPlayingPanel;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.map.MapPanel;
|
||||
import io.github.dsheirer.map.MapService;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerViewPanel;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.Scene;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
@@ -44,52 +46,50 @@ import javax.swing.Icon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
public class ControllerPanel extends JPanel
|
||||
public class ControllerPanel extends JPanel implements IPanelRevalidationRequestListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ControllerPanel.class);
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int mSettingsTabIndex = -1;
|
||||
|
||||
@Resource
|
||||
private AudioPanel mAudioPanel;
|
||||
private NowPlayingPanel mNowPlayingPanel;
|
||||
@Resource
|
||||
private CallView mCallView;
|
||||
@Resource
|
||||
private MapPanel mMapPanel;
|
||||
@Resource
|
||||
private TunerViewPanel mTunerManagerPanel;
|
||||
|
||||
@Resource
|
||||
private NowPlayingPanel mNowPlayingPanel;
|
||||
@Resource
|
||||
private AudioPlaybackChannelsView mAudioPlaybackChannelsView;
|
||||
private JideTabbedPane mTabbedPane;
|
||||
|
||||
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
||||
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
||||
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
|
||||
{
|
||||
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
||||
playlistManager.getAliasModel());
|
||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager, detailTabsVisible);
|
||||
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
|
||||
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
||||
|
||||
init();
|
||||
}
|
||||
private JFXPanel mJFXCallViewPanel;
|
||||
private JFXPanel mJFXAudioPlaybackViewPanel;
|
||||
private int mPlaylistEditorTabIndex = -1;
|
||||
|
||||
/**
|
||||
* Now playing panel.
|
||||
* Constructs an instance
|
||||
*/
|
||||
public NowPlayingPanel getNowPlayingPanel()
|
||||
public ControllerPanel()
|
||||
{
|
||||
return mNowPlayingPanel;
|
||||
}
|
||||
|
||||
private void init()
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[shrink 0]0[grow,fill]0[]"));
|
||||
|
||||
add(mAudioPanel, "wrap");
|
||||
// add(mAudioPanel, "wrap");
|
||||
add(getAudioPlaybackViewPanel(), "wrap");
|
||||
mAudioPlaybackChannelsView.setPanelRevalidationRequestListener(this);
|
||||
|
||||
mTabbedPane = new JideTabbedPane()
|
||||
{
|
||||
@Override
|
||||
public void setSelectedIndex(int index)
|
||||
{
|
||||
if(index == mSettingsTabIndex)
|
||||
if(index == mPlaylistEditorTabIndex)
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest());
|
||||
}
|
||||
@@ -101,17 +101,72 @@ public class ControllerPanel extends JPanel
|
||||
};
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
mTabbedPane.addTab("Now Playing", mNowPlayingPanel);
|
||||
mTabbedPane.addTab("Now Playing", getNowPlayingPanel());
|
||||
mTabbedPane.addTab("Calls", getCallViewPanel());
|
||||
mTabbedPane.addTab("Map", mMapPanel);
|
||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||
|
||||
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
|
||||
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
|
||||
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
||||
mPlaylistEditorTabIndex = mTabbedPane.getTabCount() - 1;
|
||||
|
||||
//Set preferred size to influence the split between these panels
|
||||
mTabbedPane.setPreferredSize(new Dimension(880, 500));
|
||||
|
||||
add(mTabbedPane, "wrap");
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements listener interface to revalidate JavaFX panel when the audio channel view style has changed.
|
||||
*/
|
||||
@Override
|
||||
public void revalidatePanel()
|
||||
{
|
||||
Platform.runLater(() ->
|
||||
{
|
||||
//Hack: this is very heavy-handed, however I couldn't find a way to get the JFXPanel to resize the height
|
||||
//when the child content changes with the style of the audio playback panels. Nonetheless, this works.
|
||||
Scene scene = getAudioPlaybackViewPanel().getScene();
|
||||
getAudioPlaybackViewPanel().setScene(null);
|
||||
getAudioPlaybackViewPanel().setScene(scene);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Now playing panel.
|
||||
*/
|
||||
public NowPlayingPanel getNowPlayingPanel()
|
||||
{
|
||||
return mNowPlayingPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call view panel wrapped in an JFXPanel. This is lazy constructed on the JavaFX platform thread.
|
||||
* @return constructed JFXPanel.
|
||||
*/
|
||||
private JFXPanel getCallViewPanel()
|
||||
{
|
||||
createJFXPanels();
|
||||
return mJFXCallViewPanel;
|
||||
}
|
||||
|
||||
private JFXPanel getAudioPlaybackViewPanel()
|
||||
{
|
||||
createJFXPanels();
|
||||
return mJFXAudioPlaybackViewPanel;
|
||||
}
|
||||
|
||||
private void createJFXPanels()
|
||||
{
|
||||
if(mJFXCallViewPanel == null || mJFXAudioPlaybackViewPanel == null)
|
||||
{
|
||||
mJFXAudioPlaybackViewPanel = new JFXPanel();
|
||||
mJFXCallViewPanel = new JFXPanel();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
mJFXCallViewPanel.setScene(new Scene(mCallView));
|
||||
mJFXAudioPlaybackViewPanel.setScene(new Scene(mAudioPlaybackChannelsView));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.controller.channel;
|
||||
|
||||
@@ -26,29 +23,32 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Channel Model
|
||||
*/
|
||||
@Component("channelModel")
|
||||
public class ChannelModel implements Listener<ChannelEvent>
|
||||
{
|
||||
private ObservableList<Channel> mChannels = FXCollections.observableArrayList(Channel.extractor());
|
||||
private ObservableList<Channel> mTrafficChannels = FXCollections.observableArrayList(Channel.extractor());
|
||||
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
|
||||
public ChannelModel(AliasModel aliasModel)
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public ChannelModel()
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
|
||||
//Register a listener to detect channel changes and broadcast change events to cause playlist save requests
|
||||
ChannelListChangeListener changeListener = new ChannelListChangeListener();
|
||||
mChannels.addListener(changeListener);
|
||||
|
||||
@@ -20,19 +20,20 @@ package io.github.dsheirer.controller.channel;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.audio.AudioManager;
|
||||
import io.github.dsheirer.audio.call.AudioSegment;
|
||||
import io.github.dsheirer.channel.metadata.ChannelAndMetadata;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadata;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.map.MapService;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.ProcessingChain;
|
||||
import io.github.dsheirer.module.decode.DecoderFactory;
|
||||
@@ -50,6 +51,8 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -63,6 +66,7 @@ import java.util.concurrent.locks.ReentrantLock;
|
||||
import javafx.application.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Channel processing manager handles all starting and stopping of channel decoding. A processing chain is created
|
||||
@@ -70,53 +74,49 @@ import org.slf4j.LoggerFactory;
|
||||
* channel and protocol along with all logging and baseband or bitstream recording. Audio recording is handled outside
|
||||
* of this class by the RecorderManager.
|
||||
*/
|
||||
@Component("channelProcessingManager")
|
||||
public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelProcessingManager.class);
|
||||
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
|
||||
private Map<Channel,ProcessingChain> mProcessingChains = new ConcurrentHashMap<>();
|
||||
private Lock mLock = new ReentrantLock();
|
||||
|
||||
private ChannelSourceEventErrorListener mSourceErrorListener = new ChannelSourceEventErrorListener();
|
||||
private List<Listener<AudioSegment>> mAudioSegmentListeners = new CopyOnWriteArrayList<>();
|
||||
private List<Listener<IDecodeEvent>> mDecodeEventListeners = new CopyOnWriteArrayList<>();
|
||||
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
|
||||
|
||||
private ChannelMapModel mChannelMapModel;
|
||||
private ChannelMetadataModel mChannelMetadataModel;
|
||||
private EventLogManager mEventLogManager;
|
||||
private TunerManager mTunerManager;
|
||||
private AliasModel mAliasModel;
|
||||
private UserPreferences mUserPreferences;
|
||||
private List<Long> mLoggedFrequencies = new ArrayList<>();
|
||||
private List<ScheduledFuture<?>> mDelayedChannelStartTasks = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs the channel processing manager
|
||||
*
|
||||
* @param channelMapModel containing channel maps defined by the user
|
||||
* @param eventLogManager for adding event loggers to channels
|
||||
* @param tunerManager for obtaining a tuner channel source for the channel
|
||||
* @param aliasModel for aliasing of identifiers produced by the channel
|
||||
* @param userPreferences for user defined behavior and settings
|
||||
*/
|
||||
public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager,
|
||||
TunerManager tunerManager, AliasModel aliasModel, UserPreferences userPreferences)
|
||||
{
|
||||
mChannelMapModel = channelMapModel;
|
||||
mEventLogManager = eventLogManager;
|
||||
mTunerManager = tunerManager;
|
||||
mAliasModel = aliasModel;
|
||||
mUserPreferences = userPreferences;
|
||||
mChannelMetadataModel = new ChannelMetadataModel();
|
||||
}
|
||||
@Resource
|
||||
private AudioManager mAudioManager;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
@Resource
|
||||
private ChannelMetadataModel mChannelMetadataModel;
|
||||
@Resource
|
||||
private DecoderFactory mDecoderFactory;
|
||||
@Resource
|
||||
private EventLogManager mEventLogManager;
|
||||
@Resource
|
||||
private MapService mMapService;
|
||||
@Resource
|
||||
private TunerManager mTunerManager;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
/**
|
||||
* Channel metadata model containing metadata for each channel or channel time-slice that is currently processing.
|
||||
* Constructs the channel processing manager
|
||||
*/
|
||||
public ChannelMetadataModel getChannelMetadataModel()
|
||||
public ChannelProcessingManager()
|
||||
{
|
||||
return mChannelMetadataModel;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
addAudioSegmentListener(mAudioManager);
|
||||
addDecodeEventListener(mMapService);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,7 +398,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
throw new ChannelException("No Tuner Available");
|
||||
}
|
||||
|
||||
ProcessingChain processingChain = new ProcessingChain(channel, mAliasModel);
|
||||
ProcessingChain processingChain = new ProcessingChain(mAliasModel, channel);
|
||||
|
||||
//Certain decoders aggregate the decode events in the parent channel that also includes any events produced
|
||||
//by the traffic channels. Establish listener registration depending on if this channel is a traffic channel
|
||||
@@ -441,8 +441,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
processingChain.addFrequencyChangeListener(channel);
|
||||
|
||||
/* Processing Modules */
|
||||
List<Module> modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences,
|
||||
request.getTrafficChannelManager(), request.getChannelDescriptor());
|
||||
List<Module> modules = mDecoderFactory.getModules(channel, request.getTrafficChannelManager(),
|
||||
request.getChannelDescriptor());
|
||||
processingChain.addModules(modules);
|
||||
|
||||
//Post preload data from the request to the event bus. Modules that can handle preload data will annotate
|
||||
@@ -560,7 +560,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
{
|
||||
added = true;
|
||||
mProcessingChains.put(channel, processingChain);
|
||||
getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
|
||||
mChannelMetadataModel.add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -590,7 +590,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
{
|
||||
for(ChannelMetadata channelMetadata: removed.getChannelState().getChannelMetadata())
|
||||
{
|
||||
getChannelMetadataModel().remove(channelMetadata);
|
||||
mChannelMetadataModel.remove(channelMetadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,20 +19,27 @@
|
||||
package io.github.dsheirer.controller.channel;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
public class ChannelSelectionManager implements Listener<ChannelEvent>
|
||||
{
|
||||
private Listener<ChannelEvent> mChannelEventListener;
|
||||
@Resource
|
||||
private ChannelModel mChannelModel;
|
||||
private Channel mSelectedChannel = null;
|
||||
|
||||
/**
|
||||
* Manages channel selection state to ensure that only one channel is ever
|
||||
* in a selected state.
|
||||
*/
|
||||
public ChannelSelectionManager(ChannelModel channelModel)
|
||||
public ChannelSelectionManager()
|
||||
{
|
||||
mChannelEventListener = channelModel;
|
||||
channelModel.addListener(ChannelSelectionManager.this);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
mChannelModel.addListener(ChannelSelectionManager.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -44,13 +51,13 @@ public class ChannelSelectionManager implements Listener<ChannelEvent>
|
||||
if(mSelectedChannel != null)
|
||||
{
|
||||
mSelectedChannel.setSelected(false);
|
||||
mChannelEventListener.receive(new ChannelEvent(mSelectedChannel, ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
|
||||
mChannelModel.receive(new ChannelEvent(mSelectedChannel, ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
|
||||
mSelectedChannel = null;
|
||||
}
|
||||
|
||||
mSelectedChannel = event.getChannel();
|
||||
mSelectedChannel.setSelected(true);
|
||||
mChannelEventListener.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
|
||||
mChannelModel.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
|
||||
break;
|
||||
case NOTIFICATION_PROCESSING_STOP:
|
||||
case REQUEST_DESELECT:
|
||||
@@ -59,7 +66,7 @@ public class ChannelSelectionManager implements Listener<ChannelEvent>
|
||||
{
|
||||
mSelectedChannel.setSelected(false);
|
||||
mSelectedChannel = null;
|
||||
mChannelEventListener.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
|
||||
mChannelModel.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.controller.channel.map;
|
||||
@@ -25,15 +22,17 @@ package io.github.dsheirer.controller.channel.map;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapEvent.Event;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.swing.AbstractListModel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component("channelMapModel")
|
||||
public class ChannelMapModel extends AbstractListModel<ChannelMap>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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,23 +14,29 @@
|
||||
*
|
||||
* 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.eventbus;
|
||||
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* System wide event bus for dispatching/broadcasting system wide events or objects
|
||||
*/
|
||||
@Component("myEventBus")
|
||||
public class MyEventBus
|
||||
{
|
||||
private static final EventBus GLOBAL_EVENT_BUS = new EventBus();
|
||||
|
||||
//TODO: this is primarily used by the Preference Service ... move this event bus to the Preference Service
|
||||
public static EventBus getGlobalEventBus()
|
||||
{
|
||||
return GLOBAL_EVENT_BUS;
|
||||
}
|
||||
|
||||
public EventBus getEventBus()
|
||||
{
|
||||
return MyEventBus.getGlobalEventBus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -20,8 +20,8 @@
|
||||
package io.github.dsheirer.gui;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMap;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
|
||||
import io.github.dsheirer.controller.channel.map.ChannelRange;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.icon.IconManager;
|
||||
@@ -37,28 +37,30 @@ import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
||||
import io.github.dsheirer.gui.viewer.RecordingViewer;
|
||||
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.jmbe.JmbeEditor;
|
||||
import io.github.dsheirer.jmbe.JmbeEditorRequest;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||
import io.github.dsheirer.monitor.StatusBox;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import jiconfont.javafx.IconFontFX;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Java FX window manager. Handles all secondary Java FX windows that are used within this primarily
|
||||
* Swing application.
|
||||
*/
|
||||
@Component("javaFxWindowManager")
|
||||
public class JavaFxWindowManager extends Application
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(JavaFxWindowManager.class);
|
||||
@@ -67,7 +69,6 @@ public class JavaFxWindowManager extends Application
|
||||
public static final String ICON_MANAGER = "iconmanager";
|
||||
public static final String PLAYLIST_EDITOR = "playlist";
|
||||
public static final String USER_PREFERENCES_EDITOR = "preferences";
|
||||
public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog";
|
||||
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
|
||||
public static final String STAGE_MONITOR_KEY_RECORDING_VIEWER = "recording.viewer";
|
||||
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
|
||||
@@ -75,50 +76,43 @@ public class JavaFxWindowManager extends Application
|
||||
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
|
||||
public static final String STAGE_MONITOR_KEY_USER_PREFERENCES_EDITOR = "user.preferences";
|
||||
|
||||
private JFXPanel mJFXPanel;
|
||||
@Resource
|
||||
private ChannelMapEditor mChannelMapEditor;
|
||||
@Resource
|
||||
private ChannelMapModel mChannelMapModel;
|
||||
@Resource
|
||||
private IconManager mIconManager;
|
||||
@Resource
|
||||
private JmbeEditor mJmbeEditor;
|
||||
@Resource
|
||||
private PlaylistEditor mPlaylistEditor;
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private TunerManager mTunerManager;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
@Resource
|
||||
private UserPreferencesEditor mUserPreferencesEditor;
|
||||
private RecordingViewer mRecordingViewer;
|
||||
|
||||
private JFXPanel mStatusPanel;
|
||||
private RecordingViewer mRecordingViewer;
|
||||
private Stage mChannelMapStage;
|
||||
private Stage mIconManagerStage;
|
||||
private Stage mJmbeEditorStage;
|
||||
private Stage mPlaylistStage;
|
||||
private Stage mUserPreferencesStage;
|
||||
private Stage mRecordingViewerStage;
|
||||
private JFXPanel mStatusPanel;
|
||||
private List<Stage> mStages = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance. Note: this constructor is used for Swing applications.
|
||||
*/
|
||||
public JavaFxWindowManager(UserPreferences userPreferences, TunerManager tunerManager, PlaylistManager playlistManager)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
mTunerManager = tunerManager;
|
||||
mPlaylistManager = playlistManager;
|
||||
|
||||
setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance. Note: this constructor is used for standalone JavaFX application testing
|
||||
*/
|
||||
public JavaFxWindowManager()
|
||||
{
|
||||
mUserPreferences = new UserPreferences();
|
||||
AliasModel aliasModel = new AliasModel();
|
||||
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
|
||||
mTunerManager = new TunerManager(mUserPreferences);
|
||||
mTunerManager.start();
|
||||
mPlaylistManager = new PlaylistManager(mUserPreferences, mTunerManager, aliasModel, eventLogManager, new IconModel());
|
||||
mPlaylistManager.init();
|
||||
setup();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
private void postConstruct()
|
||||
{
|
||||
//Register this class to receive events via each method annotated with @Subscribe
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,24 +136,11 @@ public class JavaFxWindowManager extends Application
|
||||
return mStatusPanel;
|
||||
}
|
||||
|
||||
private void setup()
|
||||
{
|
||||
//Register this class to receive events via each method annotated with @Subscribe
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
|
||||
//Register JavaFX icon fonts
|
||||
IconFontFX.register(jiconfont.icons.font_awesome.FontAwesome.getIconFont());
|
||||
|
||||
createJFXPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the runnable on the JavaFX application thread
|
||||
*/
|
||||
private void execute(Runnable runnable)
|
||||
{
|
||||
createJFXPanel();
|
||||
|
||||
if(Platform.isFxApplicationThread())
|
||||
{
|
||||
runnable.run();
|
||||
@@ -170,18 +151,6 @@ public class JavaFxWindowManager extends Application
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JavaFX panel for Swing application compatibility
|
||||
*/
|
||||
private void createJFXPanel()
|
||||
{
|
||||
if(mJFXPanel == null)
|
||||
{
|
||||
mJFXPanel = new JFXPanel();
|
||||
Platform.setImplicitExit(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes monitoring for all JavaFX stages and shuts down the FX thread, killing all FX windows.
|
||||
*/
|
||||
@@ -189,12 +158,31 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
mUserPreferences.getJavaFxPreferences().clearStageMonitors();
|
||||
Platform.exit();
|
||||
|
||||
Platform.runLater(() ->
|
||||
{
|
||||
Iterator<Stage> it = mStages.iterator();
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
try
|
||||
{
|
||||
it.next().close();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
//Do nothing ... we're shutting down
|
||||
}
|
||||
|
||||
it.remove();
|
||||
}
|
||||
|
||||
Platform.exit();
|
||||
});
|
||||
}
|
||||
|
||||
public CalibrationDialog getCalibrationDialog(UserPreferences userPreferences)
|
||||
{
|
||||
createJFXPanel();
|
||||
return new CalibrationDialog(userPreferences);
|
||||
}
|
||||
|
||||
@@ -205,11 +193,11 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
if(mRecordingViewerStage == null)
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getRecordingViewer(), 1100, 800);
|
||||
mRecordingViewerStage = new Stage();
|
||||
mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
|
||||
mRecordingViewerStage.setScene(scene);
|
||||
mStages.add(mRecordingViewerStage);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mRecordingViewerStage, STAGE_MONITOR_KEY_RECORDING_VIEWER);
|
||||
}
|
||||
|
||||
@@ -230,11 +218,11 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
if(mIconManagerStage == null)
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getIconManager(), 500, 500);
|
||||
mIconManagerStage = new Stage();
|
||||
mIconManagerStage.setTitle("sdrtrunk - Icon Manager");
|
||||
mIconManagerStage.setScene(scene);
|
||||
mStages.add(mIconManagerStage);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mIconManagerStage, STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR);
|
||||
}
|
||||
|
||||
@@ -243,11 +231,6 @@ public class JavaFxWindowManager extends Application
|
||||
|
||||
public IconManager getIconManager()
|
||||
{
|
||||
if(mIconManager == null)
|
||||
{
|
||||
mIconManager = new IconManager(mPlaylistManager.getIconModel());
|
||||
}
|
||||
|
||||
return mIconManager;
|
||||
}
|
||||
|
||||
@@ -278,11 +261,11 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
if(mJmbeEditorStage == null)
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getJmbeEditor(), 650, 650);
|
||||
mJmbeEditorStage = new Stage();
|
||||
mJmbeEditorStage.setTitle("sdrtrunk - JMBE Library Updater");
|
||||
mJmbeEditorStage.setScene(scene);
|
||||
mStages.add(mJmbeEditorStage);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mJmbeEditorStage, STAGE_MONITOR_KEY_JMBE_EDITOR);
|
||||
}
|
||||
|
||||
@@ -291,11 +274,6 @@ public class JavaFxWindowManager extends Application
|
||||
|
||||
public JmbeEditor getJmbeEditor()
|
||||
{
|
||||
if(mJmbeEditor == null)
|
||||
{
|
||||
mJmbeEditor = new JmbeEditor(mUserPreferences);
|
||||
}
|
||||
|
||||
return mJmbeEditor;
|
||||
}
|
||||
|
||||
@@ -304,11 +282,6 @@ public class JavaFxWindowManager extends Application
|
||||
*/
|
||||
public PlaylistEditor getPlaylistEditor()
|
||||
{
|
||||
if(mPlaylistEditor == null)
|
||||
{
|
||||
mPlaylistEditor = new PlaylistEditor(mPlaylistManager, mTunerManager, mUserPreferences);
|
||||
}
|
||||
|
||||
return mPlaylistEditor;
|
||||
}
|
||||
|
||||
@@ -319,11 +292,11 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
if(mPlaylistStage == null)
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getPlaylistEditor(), 1000, 750);
|
||||
mPlaylistStage = new Stage();
|
||||
mPlaylistStage.setTitle("sdrtrunk - Playlist Editor");
|
||||
mPlaylistStage.setScene(scene);
|
||||
mStages.add(mPlaylistStage);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mPlaylistStage, STAGE_MONITOR_KEY_PLAYLIST_EDITOR);
|
||||
}
|
||||
|
||||
@@ -354,11 +327,6 @@ public class JavaFxWindowManager extends Application
|
||||
*/
|
||||
private UserPreferencesEditor getUserPreferencesEditor()
|
||||
{
|
||||
if(mUserPreferencesEditor == null)
|
||||
{
|
||||
mUserPreferencesEditor = new UserPreferencesEditor(mUserPreferences);
|
||||
}
|
||||
|
||||
return mUserPreferencesEditor;
|
||||
}
|
||||
|
||||
@@ -369,11 +337,11 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
if(mUserPreferencesStage == null)
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getUserPreferencesEditor(), 900, 500);
|
||||
mUserPreferencesStage = new Stage();
|
||||
mUserPreferencesStage.setTitle("sdrtrunk - User Preferences");
|
||||
mUserPreferencesStage.setScene(scene);
|
||||
mStages.add(mUserPreferencesStage);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mUserPreferencesStage, STAGE_MONITOR_KEY_USER_PREFERENCES_EDITOR);
|
||||
}
|
||||
|
||||
@@ -397,11 +365,6 @@ public class JavaFxWindowManager extends Application
|
||||
*/
|
||||
private ChannelMapEditor getChannelMapEditor()
|
||||
{
|
||||
if(mChannelMapEditor == null)
|
||||
{
|
||||
mChannelMapEditor = new ChannelMapEditor(mPlaylistManager.getChannelMapModel());
|
||||
}
|
||||
|
||||
return mChannelMapEditor;
|
||||
}
|
||||
|
||||
@@ -412,11 +375,11 @@ public class JavaFxWindowManager extends Application
|
||||
{
|
||||
if(mChannelMapStage == null)
|
||||
{
|
||||
createJFXPanel();
|
||||
Scene scene = new Scene(getChannelMapEditor(), 500, 500);
|
||||
mChannelMapStage = new Stage();
|
||||
mChannelMapStage.setTitle("sdrtrunk - Channel Map Editor");
|
||||
mChannelMapStage.setScene(scene);
|
||||
mStages.add(mChannelMapStage);
|
||||
mUserPreferences.getJavaFxPreferences().monitor(mChannelMapStage, STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR);
|
||||
}
|
||||
|
||||
@@ -489,14 +452,14 @@ public class JavaFxWindowManager extends Application
|
||||
channelMap1.addRange(new ChannelRange(200,299,160000000, 25000));
|
||||
channelMap1.addRange(new ChannelRange(300,399,170000000, 12500));
|
||||
channelMap1.addRange(new ChannelRange(400,499,180000000, 25000));
|
||||
mPlaylistManager.getChannelMapModel().addChannelMap(channelMap1);
|
||||
mChannelMapModel.addChannelMap(channelMap1);
|
||||
|
||||
ChannelMap channelMap2 = new ChannelMap("Test Map 2");
|
||||
channelMap2.addRange(new ChannelRange(1,199,450000000, 12500));
|
||||
channelMap2.addRange(new ChannelRange(200,299,460000000, 25000));
|
||||
channelMap2.addRange(new ChannelRange(300,399,470000000, 12500));
|
||||
channelMap2.addRange(new ChannelRange(400,499,480000000, 25000));
|
||||
mPlaylistManager.getChannelMapModel().addChannelMap(channelMap2);
|
||||
mChannelMapModel.addChannelMap(channelMap2);
|
||||
valid = true;
|
||||
process(new ViewChannelMapEditorRequest());
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -18,864 +18,154 @@
|
||||
*/
|
||||
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.DuplicateCallDetector;
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
||||
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
|
||||
import io.github.dsheirer.controller.ControllerPanel;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
|
||||
import io.github.dsheirer.controller.channel.ChannelException;
|
||||
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
|
||||
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
|
||||
import io.github.dsheirer.gui.preference.CalibrateRequest;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
||||
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.controller.channel.ChannelModel;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.log.ApplicationLog;
|
||||
import io.github.dsheirer.map.MapService;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.record.AudioRecordingManager;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.tuner.Tuner;
|
||||
import io.github.dsheirer.source.tuner.TunerEvent;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
||||
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
|
||||
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import java.awt.AWTException;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.Dimension;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.awt.Point;
|
||||
import java.awt.Robot;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.prefs.Preferences;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.Banner;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.domain.EntityScan;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JCheckBoxMenuItem;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuBar;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.event.MenuEvent;
|
||||
import javax.swing.event.MenuListener;
|
||||
import javax.swing.plaf.metal.MetalLookAndFeel;
|
||||
|
||||
public class SDRTrunk implements Listener<TunerEvent>
|
||||
@SpringBootApplication(scanBasePackages = {"io.github.dsheirer"})
|
||||
@EnableJpaRepositories("io.github.dsheirer.audio.call")
|
||||
@EntityScan("io.github.dsheirer.audio.call")
|
||||
public class SDRTrunk
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
|
||||
private Preferences mPreferences = Preferences.userNodeForPackage(SDRTrunk.class);
|
||||
|
||||
private static final String PREFERENCE_BROADCAST_STATUS_VISIBLE = "sdrtrunk.broadcast.status.visible";
|
||||
private static final String PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE = "sdrtrunk.now.playing.details.visible";
|
||||
private static final String PREFERENCE_RESOURCE_STATUS_VISIBLE = "sdrtrunk.resource.status.visible";
|
||||
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
|
||||
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
|
||||
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
|
||||
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||
|
||||
private boolean mBroadcastStatusVisible;
|
||||
private boolean mResourceStatusVisible;
|
||||
private boolean mNowPlayingDetailsVisible;
|
||||
private AudioRecordingManager mAudioRecordingManager;
|
||||
private AudioStreamingManager mAudioStreamingManager;
|
||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||
private ControllerPanel mControllerPanel;
|
||||
private IconModel mIconModel = new IconModel();
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private SettingsManager mSettingsManager;
|
||||
private SpectralDisplayPanel mSpectralPanel;
|
||||
private JFrame mMainGui;
|
||||
private JideSplitPane mSplitPane;
|
||||
private JavaFxWindowManager mJavaFxWindowManager;
|
||||
private UserPreferences mUserPreferences = new UserPreferences();
|
||||
private TunerManager mTunerManager;
|
||||
@Resource
|
||||
private ApplicationLog mApplicationLog;
|
||||
private ResourceMonitor mResourceMonitor;
|
||||
private JFXPanel mResourceStatusPanel;
|
||||
|
||||
private String mTitle;
|
||||
@Resource
|
||||
private AudioRecordingManager mAudioRecordingManager;
|
||||
@Resource
|
||||
private ChannelModel mChannelModel;
|
||||
@Resource
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
@Resource
|
||||
private TunerManager mTunerManager;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
@Resource //SpringUiFactory does not instantiate this instance when headless=true
|
||||
private SDRTrunkUI mSDRTrunkUI;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the SDRTrunk application
|
||||
*/
|
||||
public SDRTrunk()
|
||||
{
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
mMainGui = new JFrame();
|
||||
}
|
||||
}
|
||||
|
||||
mApplicationLog = new ApplicationLog(mUserPreferences);
|
||||
mApplicationLog.start();
|
||||
|
||||
mResourceMonitor = new ResourceMonitor(mUserPreferences);
|
||||
|
||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
||||
{
|
||||
try
|
||||
{
|
||||
UIManager.setLookAndFeel(MetalLookAndFeel.class.getName());
|
||||
LookAndFeelFactory.installJideExtension();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error trying to set Metal look and feel for OS [" + operatingSystem + "]");
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
ThreadPool.logSettings();
|
||||
|
||||
//Load properties file
|
||||
loadProperties();
|
||||
|
||||
//Log current properties setting
|
||||
SystemProperties.getInstance().logCurrentSettings();
|
||||
|
||||
//Register FontAwesome so we can use the fonts in Swing windows
|
||||
IconFontSwing.register(FontAwesome.getIconFont());
|
||||
|
||||
mTunerManager = new TunerManager(mUserPreferences);
|
||||
mTunerManager.start();
|
||||
|
||||
mSettingsManager = new SettingsManager();
|
||||
|
||||
AliasModel aliasModel = new AliasModel();
|
||||
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
|
||||
mPlaylistManager = new PlaylistManager(mUserPreferences, mTunerManager, aliasModel, eventLogManager, mIconModel);
|
||||
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences, mTunerManager, mPlaylistManager);
|
||||
}
|
||||
|
||||
CalibrationManager calibrationManager = CalibrationManager.getInstance(mUserPreferences);
|
||||
final boolean calibrating = !calibrationManager.isCalibrated() &&
|
||||
!mUserPreferences.getVectorCalibrationPreference().isHideCalibrationDialog();
|
||||
|
||||
new ChannelSelectionManager(mPlaylistManager.getChannelModel());
|
||||
|
||||
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mUserPreferences);
|
||||
|
||||
mAudioRecordingManager = new AudioRecordingManager(mUserPreferences);
|
||||
mAudioRecordingManager.start();
|
||||
|
||||
mAudioStreamingManager = new AudioStreamingManager(mPlaylistManager.getBroadcastModel(), BroadcastFormat.MP3,
|
||||
mUserPreferences);
|
||||
mAudioStreamingManager.start();
|
||||
|
||||
DuplicateCallDetector duplicateCallDetector = new DuplicateCallDetector(mUserPreferences);
|
||||
|
||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(duplicateCallDetector);
|
||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(audioPlaybackManager);
|
||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
|
||||
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);
|
||||
|
||||
MapService mapService = new MapService(mIconModel);
|
||||
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
|
||||
|
||||
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
|
||||
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
|
||||
mSettingsManager, mTunerManager, mUserPreferences, mNowPlayingDetailsVisible);
|
||||
}
|
||||
|
||||
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
||||
|
||||
TunerSpectralDisplayManager tunerSpectralDisplayManager = new TunerSpectralDisplayManager(mSpectralPanel,
|
||||
mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
|
||||
mTunerManager.getDiscoveredTunerModel().addListener(tunerSpectralDisplayManager);
|
||||
mTunerManager.getDiscoveredTunerModel().addListener(this);
|
||||
|
||||
mPlaylistManager.init();
|
||||
|
||||
if(GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
mLog.info("starting main application headless");
|
||||
mLog.info("starting main application in headless mode");
|
||||
autoStartChannels();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("starting main application gui");
|
||||
|
||||
//Initialize the GUI
|
||||
initGUI();
|
||||
}
|
||||
|
||||
//Start the gui
|
||||
EventQueue.invokeLater(() -> {
|
||||
try
|
||||
mLog.info("starting main application with gui");
|
||||
if(mSDRTrunkUI != null)
|
||||
{
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
mMainGui.setVisible(true);
|
||||
Tuner tuner = tunerSpectralDisplayManager.showFirstTuner();
|
||||
|
||||
if(tuner != null)
|
||||
{
|
||||
updateTitle(tuner.getPreferredName());
|
||||
}
|
||||
}
|
||||
|
||||
if(calibrating && !GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
Platform.runLater(() ->
|
||||
{
|
||||
CalibrationDialog calibrationDialog = mJavaFxWindowManager.getCalibrationDialog(mUserPreferences);
|
||||
Optional<ButtonType> calibrate = calibrationDialog.showAndWait();
|
||||
if(calibrate.isPresent() && calibrate.get().getText().equals("Calibrate"))
|
||||
{
|
||||
//Request focus and execute calibration
|
||||
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.VECTOR_CALIBRATION));
|
||||
MyEventBus.getGlobalEventBus().post(new CalibrateRequest());
|
||||
}
|
||||
else
|
||||
{
|
||||
autoStartChannels();
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
autoStartChannels();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog that lists the channels that have been designated for auto-start, sorted by auto-start order and
|
||||
* allows the user to start now, cancel, or allow the timer to expire and then start the channels. The dialog will
|
||||
* only show if there are one ore more channels designated for auto-start.
|
||||
*/
|
||||
private void autoStartChannels()
|
||||
{
|
||||
List<Channel> channels = mPlaylistManager.getChannelModel().getAutoStartChannels();
|
||||
|
||||
if(channels.size() > 0)
|
||||
{
|
||||
if(GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
for(Channel channel: channels)
|
||||
{
|
||||
try
|
||||
{
|
||||
mLog.info("Auto-starting channel " + channel.getName());
|
||||
mPlaylistManager.getChannelProcessingManager().start(channel);
|
||||
}
|
||||
catch(ChannelException ce)
|
||||
{
|
||||
mLog.error("Channel: " + channel.getName() + " auto-start failed: " + ce.getMessage());
|
||||
}
|
||||
}
|
||||
EventQueue.invokeLater(() -> mSDRTrunkUI.setVisible(true));
|
||||
}
|
||||
else
|
||||
{
|
||||
new ChannelAutoStartFrame(mPlaylistManager.getChannelProcessingManager(), channels, mUserPreferences);
|
||||
mLog.error("SDRTrunk user interface is null - can't start UI");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the contents of the frame.
|
||||
* Starts processing any channel configurations that are designated to auto-start
|
||||
*/
|
||||
private void initGUI()
|
||||
private void autoStartChannels()
|
||||
{
|
||||
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]0[shrink 0]"));
|
||||
|
||||
/**
|
||||
* Setup main JFrame window
|
||||
*/
|
||||
mTitle = SystemProperties.getInstance().getApplicationName();
|
||||
mMainGui.setTitle(mTitle);
|
||||
|
||||
Point location = mUserPreferences.getSwingPreference().getLocation(WINDOW_FRAME_IDENTIFIER);
|
||||
if(location != null)
|
||||
List<Channel> channels = mChannelModel.getAutoStartChannels();
|
||||
for(Channel channel: channels)
|
||||
{
|
||||
mMainGui.setLocation(location);
|
||||
}
|
||||
else
|
||||
{
|
||||
mMainGui.setLocationRelativeTo(null);
|
||||
}
|
||||
mMainGui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
mMainGui.addWindowListener(new ShutdownMonitor());
|
||||
|
||||
Dimension dimension = mUserPreferences.getSwingPreference().getDimension(WINDOW_FRAME_IDENTIFIER);
|
||||
|
||||
mSpectralPanel.setPreferredSize(new Dimension(1280, 300));
|
||||
mControllerPanel.setPreferredSize(new Dimension(1280, 500));
|
||||
|
||||
if(dimension != null)
|
||||
{
|
||||
Dimension spectral = mUserPreferences.getSwingPreference().getDimension(SPECTRAL_PANEL_IDENTIFIER);
|
||||
if(spectral != null)
|
||||
{
|
||||
mSpectralPanel.setSize(spectral);
|
||||
}
|
||||
|
||||
Dimension controller = mUserPreferences.getSwingPreference().getDimension(CONTROLLER_PANEL_IDENTIFIER);
|
||||
if(controller != null)
|
||||
{
|
||||
mControllerPanel.setSize(controller);
|
||||
}
|
||||
|
||||
mMainGui.setSize(dimension);
|
||||
|
||||
if(mUserPreferences.getSwingPreference().getMaximized(WINDOW_FRAME_IDENTIFIER, false))
|
||||
{
|
||||
mMainGui.setExtendedState(JFrame.MAXIMIZED_BOTH);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mMainGui.setSize(new Dimension(1280, 800));
|
||||
}
|
||||
|
||||
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
mSplitPane.setDividerSize(5);
|
||||
mSplitPane.add(mSpectralPanel);
|
||||
mSplitPane.add(mControllerPanel);
|
||||
|
||||
mBroadcastStatusVisible = mPreferences.getBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, false);
|
||||
|
||||
//Show broadcast status panel when user requests - disabled by default
|
||||
if(mBroadcastStatusVisible)
|
||||
{
|
||||
mSplitPane.add(getBroadcastStatusPanel());
|
||||
}
|
||||
|
||||
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
|
||||
|
||||
mResourceMonitor.start();
|
||||
mResourceStatusVisible = mPreferences.getBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, true);
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu items
|
||||
*/
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
mMainGui.setJMenuBar(menuBar);
|
||||
|
||||
JMenu fileMenu = new JMenu("File");
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
JMenuItem exitMenu = new JMenuItem("Exit");
|
||||
exitMenu.addActionListener(event -> {
|
||||
processShutdown();
|
||||
System.exit(0);
|
||||
}
|
||||
);
|
||||
|
||||
fileMenu.add(exitMenu);
|
||||
|
||||
JMenu viewMenu = new JMenu("View");
|
||||
|
||||
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
||||
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
|
||||
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
||||
viewMenu.add(viewPlaylistItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
|
||||
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
|
||||
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewApplicationLogsMenu.addActionListener(arg0 -> {
|
||||
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(logsDirectory);
|
||||
mLog.info("Auto-starting channel " + channel.getName());
|
||||
mChannelProcessingManager.start(channel);
|
||||
}
|
||||
catch(Exception e)
|
||||
catch(ChannelException ce)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + logsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
mLog.error("Channel: " + channel.getName() + " auto-start failed: " + ce.getMessage());
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewApplicationLogsMenu);
|
||||
|
||||
JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings");
|
||||
viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewRecordingsMenuItem.addActionListener(arg0 -> {
|
||||
File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile();
|
||||
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(recordingsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " +
|
||||
recordingsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewRecordingsMenuItem);
|
||||
|
||||
JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files");
|
||||
viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewEventLogsMenu.addActionListener(arg0 -> {
|
||||
File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(eventLogsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + eventLogsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewEventLogsMenu);
|
||||
|
||||
JMenuItem iconManagerMenu = new JMenuItem("Icon Manager");
|
||||
iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12));
|
||||
iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||
viewMenu.add(iconManagerMenu);
|
||||
|
||||
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
|
||||
recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12));
|
||||
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
|
||||
viewMenu.add(recordingViewerMenu);
|
||||
|
||||
JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures");
|
||||
viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewScreenCapturesMenu.addActionListener(arg0 -> {
|
||||
File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(screenCapturesDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(mMainGui,
|
||||
"Can't launch file explorer - files are located at: " + screenCapturesDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewScreenCapturesMenu);
|
||||
|
||||
JMenuItem preferencesItem = new JMenuItem("User Preferences");
|
||||
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
|
||||
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
||||
viewMenu.add(preferencesItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new TunersMenu());
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel));
|
||||
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
|
||||
viewMenu.add(new BroadcastStatusVisibleMenuItem());
|
||||
viewMenu.add(new ResourceStatusVisibleMenuItem());
|
||||
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
||||
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
|
||||
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
||||
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
||||
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
||||
screenCaptureItem.addActionListener(arg0 -> {
|
||||
try
|
||||
{
|
||||
Robot robot = new Robot();
|
||||
|
||||
final BufferedImage image = robot.createScreenCapture(mMainGui.getBounds());
|
||||
|
||||
String filename = TimeStamp.getTimeStamp("_") + "_screen_capture.png";
|
||||
|
||||
final Path captureFile = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().resolve(filename);
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
try
|
||||
{
|
||||
ImageIO.write(image, "png", captureFile.toFile());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Couldn't write screen capture to file [" + captureFile.toString() + "]", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(AWTException e)
|
||||
{
|
||||
mLog.error("Exception while taking screen capture", e);
|
||||
}
|
||||
});
|
||||
|
||||
menuBar.add(screenCaptureItem);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs shutdown operations
|
||||
*/
|
||||
private void processShutdown()
|
||||
@PreDestroy
|
||||
public void preDestroy()
|
||||
{
|
||||
mLog.info("Application shutdown started ...");
|
||||
mUserPreferences.getSwingPreference().setLocation(WINDOW_FRAME_IDENTIFIER, mMainGui.getLocation());
|
||||
mUserPreferences.getSwingPreference().setDimension(WINDOW_FRAME_IDENTIFIER, mMainGui.getSize());
|
||||
mUserPreferences.getSwingPreference().setMaximized(WINDOW_FRAME_IDENTIFIER,
|
||||
(mMainGui.getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH);
|
||||
mUserPreferences.getSwingPreference().setDimension(SPECTRAL_PANEL_IDENTIFIER, mSpectralPanel.getSize());
|
||||
mUserPreferences.getSwingPreference().setDimension(CONTROLLER_PANEL_IDENTIFIER, mControllerPanel.getSize());
|
||||
mJavaFxWindowManager.shutdown();
|
||||
mLog.info("Stopping channels ...");
|
||||
mPlaylistManager.getChannelProcessingManager().shutdown();
|
||||
mChannelProcessingManager.shutdown();
|
||||
mAudioRecordingManager.stop();
|
||||
mResourceMonitor.stop();
|
||||
|
||||
mLog.info("Stopping spectral display ...");
|
||||
mSpectralPanel.clearTuner();
|
||||
mLog.info("Stopping tuners ...");
|
||||
mTunerManager.stop();
|
||||
mLog.info("Shutdown complete.");
|
||||
mApplicationLog.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy constructor for broadcast status panel
|
||||
* Monitors the SDRTrunkUI window for shutdown so that we can terminate the overall SDRTrunk application.
|
||||
*/
|
||||
private BroadcastStatusPanel getBroadcastStatusPanel()
|
||||
{
|
||||
if(mBroadcastStatusPanel == null)
|
||||
{
|
||||
mBroadcastStatusPanel = new BroadcastStatusPanel(mPlaylistManager.getBroadcastModel(), mUserPreferences,
|
||||
"application.broadcast.status.panel");
|
||||
mBroadcastStatusPanel.setPreferredSize(new Dimension(880, 70));
|
||||
mBroadcastStatusPanel.getTable().setEnabled(false);
|
||||
}
|
||||
|
||||
return mBroadcastStatusPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the broadcast channels status panel at the bottom of the controller panel
|
||||
*/
|
||||
private void toggleBroadcastStatusPanelVisibility()
|
||||
{
|
||||
mBroadcastStatusVisible = !mBroadcastStatusVisible;
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
if(mBroadcastStatusVisible)
|
||||
{
|
||||
mSplitPane.add(getBroadcastStatusPanel());
|
||||
}
|
||||
else
|
||||
{
|
||||
mSplitPane.remove(getBroadcastStatusPanel());
|
||||
}
|
||||
|
||||
mMainGui.revalidate();
|
||||
});
|
||||
|
||||
mPreferences.putBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy constructor for resource status panel
|
||||
*/
|
||||
private JFXPanel getResourceStatusPanel()
|
||||
{
|
||||
|
||||
if(mResourceStatusPanel == null)
|
||||
{
|
||||
mResourceStatusPanel = mJavaFxWindowManager.getStatusPanel(mResourceMonitor);
|
||||
}
|
||||
|
||||
return mResourceStatusPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the resource status panel at the bottom of the main UI window
|
||||
*/
|
||||
private void toggleResourceStatusPanelVisibility()
|
||||
{
|
||||
mResourceStatusVisible = !mResourceStatusVisible;
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
mMainGui.add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
else
|
||||
{
|
||||
mMainGui.remove(getResourceStatusPanel());
|
||||
}
|
||||
|
||||
mMainGui.revalidate();
|
||||
});
|
||||
|
||||
mPreferences.putBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, mResourceStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the Now Playing channel details panel
|
||||
*/
|
||||
private void toggleNowPlayingDetailsPanelVisibility()
|
||||
{
|
||||
mNowPlayingDetailsVisible = !mNowPlayingDetailsVisible;
|
||||
mControllerPanel.getNowPlayingPanel().setDetailTabsVisible(mNowPlayingDetailsVisible);
|
||||
mPreferences.putBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, mNowPlayingDetailsVisible);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the application properties file from the user's home directory,
|
||||
* creating the properties file for the first-time, if necessary
|
||||
*/
|
||||
private void loadProperties()
|
||||
{
|
||||
Path propertiesPath = mUserPreferences.getDirectoryPreference().getDirectoryApplicationRoot().resolve("SDRTrunk.properties");
|
||||
|
||||
if(!Files.exists(propertiesPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
mLog.info("SDRTrunk - creating application properties file [" + propertiesPath.toAbsolutePath() + "]");
|
||||
Files.createFile(propertiesPath);
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("SDRTrunk - couldn't create application properties file [" + propertiesPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if(Files.exists(propertiesPath))
|
||||
{
|
||||
SystemProperties.getInstance().load(propertiesPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk application properties file");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets (or creates) the SDRTRunk application home directory.
|
||||
*
|
||||
* Note: the user can change this setting to allow log files and other
|
||||
* files to reside elsewhere on the file system.
|
||||
*/
|
||||
private Path getHomePath()
|
||||
{
|
||||
Path homePath = FileSystems.getDefault()
|
||||
.getPath(System.getProperty("user.home"), "SDRTrunk");
|
||||
|
||||
if(!Files.exists(homePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Files.createDirectory(homePath);
|
||||
|
||||
mLog.info("SDRTrunk - created application home directory [" +
|
||||
homePath.toString() + "]");
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
homePath = null;
|
||||
|
||||
mLog.error("SDRTrunk: exception while creating SDRTrunk home " +
|
||||
"directory in the user's home directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
return homePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(TunerEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case REQUEST_MAIN_SPECTRAL_DISPLAY:
|
||||
updateTitle(event.getTuner().getPreferredName());
|
||||
break;
|
||||
case REQUEST_CLEAR_MAIN_SPECTRAL_DISPLAY:
|
||||
updateTitle(null);
|
||||
break;
|
||||
case NOTIFICATION_SHUTTING_DOWN:
|
||||
Tuner currentTuner = mSpectralPanel.getTuner();
|
||||
|
||||
if(event.hasTuner() && event.getTuner().equals(currentTuner) || currentTuner == null)
|
||||
{
|
||||
updateTitle(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title bar with the tuner name
|
||||
* @param tunerName optional
|
||||
*/
|
||||
private void updateTitle(String tunerName)
|
||||
{
|
||||
if(tunerName != null)
|
||||
{
|
||||
mMainGui.setTitle(mTitle + " - " + tunerName);
|
||||
}
|
||||
else
|
||||
{
|
||||
mMainGui.setTitle(mTitle);
|
||||
}
|
||||
}
|
||||
|
||||
public class ShutdownMonitor extends WindowAdapter
|
||||
{
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e)
|
||||
{
|
||||
processShutdown();
|
||||
preDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast status panel visible toggle menu item
|
||||
*/
|
||||
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public BroadcastStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Streaming Status");
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleBroadcastStatusPanelVisibility();
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource status panel visible toggle menu item
|
||||
*/
|
||||
public class ResourceStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public ResourceStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Resource Status");
|
||||
setSelected(mResourceStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleResourceStatusPanelVisibility();
|
||||
setSelected(mResourceStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now Playing channel details visible toggle menu item
|
||||
*/
|
||||
public class NowPlayingChannelDetailsVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public NowPlayingChannelDetailsVisibleMenuItem()
|
||||
{
|
||||
super("Show Now Playing Channel Details");
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
addActionListener(e -> {
|
||||
toggleNowPlayingDetailsPanelVisibility();
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public class TunersMenu extends JMenu
|
||||
{
|
||||
public TunersMenu()
|
||||
{
|
||||
super("Tuners");
|
||||
|
||||
addMenuListener(new MenuListener()
|
||||
{
|
||||
@Override
|
||||
public void menuSelected(MenuEvent e)
|
||||
{
|
||||
removeAll();
|
||||
|
||||
for(DiscoveredTuner discoveredTuner: mTunerManager.getAvailableTuners())
|
||||
{
|
||||
add(new ShowTunerMenuItem(mTunerManager.getDiscoveredTunerModel(), discoveredTuner.getTuner()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuDeselected(MenuEvent e) { }
|
||||
@Override
|
||||
public void menuCanceled(MenuEvent e) { }
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch the application.
|
||||
*/
|
||||
public static void main(String[] args)
|
||||
{
|
||||
new SDRTrunk();
|
||||
boolean headless = GraphicsEnvironment.isHeadless();
|
||||
|
||||
//Set the user's preferred location for the database prior to starting the application
|
||||
// String dbpath = new UserPreferences().getDirectoryPreference().getDirectoryApplicationRoot().toString();
|
||||
String dbpath = new UserPreferences().getDirectoryPreference().getDirectoryDatabase().getParent().toString();
|
||||
System.setProperty("derby.system.home", dbpath);
|
||||
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder(SDRTrunk.class)
|
||||
.bannerMode(Banner.Mode.OFF)
|
||||
.headless(headless)
|
||||
.web(WebApplicationType.NONE)
|
||||
.registerShutdownHook(true)
|
||||
.run(args);
|
||||
}
|
||||
}
|
||||
|
||||
709
src/main/java/io/github/dsheirer/gui/SDRTrunkUI.java
Normal file
709
src/main/java/io/github/dsheirer/gui/SDRTrunkUI.java
Normal file
@@ -0,0 +1,709 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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;
|
||||
|
||||
import com.jidesoft.plaf.LookAndFeelFactory;
|
||||
import com.jidesoft.swing.JideSplitPane;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
||||
import io.github.dsheirer.controller.ControllerPanel;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
|
||||
import io.github.dsheirer.controller.channel.ChannelModel;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
|
||||
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
|
||||
import io.github.dsheirer.gui.preference.CalibrateRequest;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
|
||||
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
|
||||
import io.github.dsheirer.monitor.ResourceMonitor;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.tuner.Tuner;
|
||||
import io.github.dsheirer.source.tuner.TunerEvent;
|
||||
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
|
||||
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
|
||||
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
|
||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.AWTException;
|
||||
import java.awt.Desktop;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Point;
|
||||
import java.awt.Robot;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.prefs.Preferences;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JCheckBoxMenuItem;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuBar;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.UIManager;
|
||||
import javax.swing.event.MenuEvent;
|
||||
import javax.swing.event.MenuListener;
|
||||
import javax.swing.plaf.metal.MetalLookAndFeel;
|
||||
|
||||
/**
|
||||
* SDRTrunk primary user interface frame/window
|
||||
*/
|
||||
public class SDRTrunkUI extends JFrame implements Listener<TunerEvent>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(SDRTrunkUI.class);
|
||||
private final Preferences mPreferences = Preferences.userNodeForPackage(SDRTrunkUI.class);
|
||||
|
||||
private static final String PREFERENCE_BROADCAST_STATUS_VISIBLE = "sdrtrunk.broadcast.status.visible";
|
||||
private static final String PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE = "sdrtrunk.now.playing.details.visible";
|
||||
private static final String PREFERENCE_RESOURCE_STATUS_VISIBLE = "sdrtrunk.resource.status.visible";
|
||||
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
|
||||
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
|
||||
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
|
||||
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||
|
||||
@Resource
|
||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||
@Resource
|
||||
private ChannelModel mChannelModel;
|
||||
@Resource
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
@Resource
|
||||
private ControllerPanel mControllerPanel;
|
||||
@Resource
|
||||
private DiscoveredTunerModel mDiscoveredTunerModel;
|
||||
@Resource
|
||||
private JavaFxWindowManager mJavaFxWindowManager;
|
||||
@Resource
|
||||
private ResourceMonitor mResourceMonitor;
|
||||
@Resource
|
||||
private SpectralDisplayPanel mSpectralPanel;
|
||||
@Resource
|
||||
private TunerSpectralDisplayManager mTunerSpectralDisplayManager;
|
||||
@Resource
|
||||
private TunerManager mTunerManager;
|
||||
|
||||
private JFXPanel mResourceStatusPanel;
|
||||
private JideSplitPane mSplitPane;
|
||||
private boolean mBroadcastStatusVisible;
|
||||
private boolean mResourceStatusVisible;
|
||||
private boolean mNowPlayingDetailsVisible;
|
||||
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
private String mTitle;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the SDRTrunk user interface
|
||||
*/
|
||||
public SDRTrunkUI()
|
||||
{
|
||||
//Register FontAwesome so we can use the fonts in Swing windows
|
||||
IconFontSwing.register(FontAwesome.getIconFont());
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into this visible method so that we can show the auto-start channels dialog on window show.
|
||||
*/
|
||||
@Override
|
||||
public void setVisible(boolean b)
|
||||
{
|
||||
super.setVisible(b);
|
||||
|
||||
CalibrationManager calibrationManager = CalibrationManager.getInstance(mUserPreferences);
|
||||
final boolean calibrating = !calibrationManager.isCalibrated() &&
|
||||
!mUserPreferences.getVectorCalibrationPreference().isHideCalibrationDialog();
|
||||
|
||||
if(calibrating)
|
||||
{
|
||||
Platform.runLater(() ->
|
||||
{
|
||||
CalibrationDialog calibrationDialog = mJavaFxWindowManager.getCalibrationDialog(mUserPreferences);
|
||||
Optional<ButtonType> calibrate = calibrationDialog.showAndWait();
|
||||
if(calibrate.isPresent() && calibrate.get().getText().equals("Calibrate"))
|
||||
{
|
||||
//Request focus and execute calibration
|
||||
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.VECTOR_CALIBRATION));
|
||||
MyEventBus.getGlobalEventBus().post(new CalibrateRequest());
|
||||
}
|
||||
else
|
||||
{
|
||||
autoStartChannels();
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
autoStartChannels();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the auto-start channels dialog
|
||||
*/
|
||||
private void autoStartChannels()
|
||||
{
|
||||
List<Channel> channels = mChannelModel.getAutoStartChannels();
|
||||
|
||||
if(channels.size() > 0)
|
||||
{
|
||||
new ChannelAutoStartFrame(mChannelProcessingManager, channels, mUserPreferences);
|
||||
}
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
|
||||
{
|
||||
try
|
||||
{
|
||||
UIManager.setLookAndFeel(MetalLookAndFeel.class.getName());
|
||||
LookAndFeelFactory.installJideExtension();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error trying to set Metal look and feel for OS [" + operatingSystem + "]");
|
||||
}
|
||||
}
|
||||
|
||||
new ChannelSelectionManager();
|
||||
|
||||
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
|
||||
mTunerSpectralDisplayManager.setSpectralDisplayPanel(mSpectralPanel);
|
||||
mDiscoveredTunerModel.addListener(mTunerSpectralDisplayManager);
|
||||
mDiscoveredTunerModel.addListener(this);
|
||||
|
||||
initGUI();
|
||||
|
||||
Tuner tuner = mTunerSpectralDisplayManager.showFirstTuner();
|
||||
|
||||
if(tuner != null)
|
||||
{
|
||||
updateTitle(tuner.getPreferredName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the contents of the frame.
|
||||
*/
|
||||
private void initGUI()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]0[shrink 0]"));
|
||||
|
||||
/**
|
||||
* Setup main JFrame window
|
||||
*/
|
||||
mTitle = SystemProperties.getInstance().getApplicationName();
|
||||
setTitle(mTitle);
|
||||
|
||||
Point location = mUserPreferences.getSwingPreference().getLocation(WINDOW_FRAME_IDENTIFIER);
|
||||
if(location != null)
|
||||
{
|
||||
setLocation(location);
|
||||
}
|
||||
else
|
||||
{
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
||||
addWindowListener(new ShutdownMonitor());
|
||||
|
||||
Dimension dimension = mUserPreferences.getSwingPreference().getDimension(WINDOW_FRAME_IDENTIFIER);
|
||||
|
||||
mSpectralPanel.setPreferredSize(new Dimension(1280, 300));
|
||||
mControllerPanel.setPreferredSize(new Dimension(1280, 500));
|
||||
|
||||
if(dimension != null)
|
||||
{
|
||||
Dimension spectral = mUserPreferences.getSwingPreference().getDimension(SPECTRAL_PANEL_IDENTIFIER);
|
||||
if(spectral != null)
|
||||
{
|
||||
mSpectralPanel.setSize(spectral);
|
||||
}
|
||||
|
||||
Dimension controller = mUserPreferences.getSwingPreference().getDimension(CONTROLLER_PANEL_IDENTIFIER);
|
||||
if(controller != null)
|
||||
{
|
||||
mControllerPanel.setSize(controller);
|
||||
}
|
||||
|
||||
setSize(dimension);
|
||||
|
||||
if(mUserPreferences.getSwingPreference().getMaximized(WINDOW_FRAME_IDENTIFIER, false))
|
||||
{
|
||||
setExtendedState(JFrame.MAXIMIZED_BOTH);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
setSize(new Dimension(1280, 800));
|
||||
}
|
||||
|
||||
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
mSplitPane.setDividerSize(5);
|
||||
mSplitPane.add(mSpectralPanel);
|
||||
mSplitPane.add(mControllerPanel);
|
||||
|
||||
mBroadcastStatusVisible = mPreferences.getBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, false);
|
||||
|
||||
//Show broadcast status panel when user requests - disabled by default
|
||||
if(mBroadcastStatusVisible)
|
||||
{
|
||||
mSplitPane.add(getBroadcastStatusPanel());
|
||||
}
|
||||
|
||||
add(mSplitPane, "cell 0 0,span,grow");
|
||||
|
||||
mResourceMonitor.start();
|
||||
mResourceStatusVisible = mPreferences.getBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, true);
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu items
|
||||
*/
|
||||
JMenuBar menuBar = new JMenuBar();
|
||||
setJMenuBar(menuBar);
|
||||
|
||||
JMenu fileMenu = new JMenu("File");
|
||||
menuBar.add(fileMenu);
|
||||
|
||||
JMenuItem exitMenu = new JMenuItem("Exit");
|
||||
exitMenu.addActionListener(event -> {
|
||||
shutdown();
|
||||
System.exit(0);
|
||||
}
|
||||
);
|
||||
|
||||
fileMenu.add(exitMenu);
|
||||
|
||||
JMenu viewMenu = new JMenu("View");
|
||||
|
||||
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
|
||||
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
|
||||
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
|
||||
viewMenu.add(viewPlaylistItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
|
||||
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
|
||||
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewApplicationLogsMenu.addActionListener(arg0 -> {
|
||||
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(logsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Can't launch file explorer - files are located at: " + logsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewApplicationLogsMenu);
|
||||
|
||||
JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings");
|
||||
viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewRecordingsMenuItem.addActionListener(arg0 -> {
|
||||
File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile();
|
||||
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(recordingsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(this,
|
||||
"Can't launch file explorer - files are located at: " +
|
||||
recordingsDirectory,
|
||||
"Can't launch file explorer",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewRecordingsMenuItem);
|
||||
|
||||
JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files");
|
||||
viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewEventLogsMenu.addActionListener(arg0 -> {
|
||||
File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(eventLogsDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(this, "Can't launch file explorer - files are located at: " + eventLogsDirectory,
|
||||
"Can't launch file explorer", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewEventLogsMenu);
|
||||
|
||||
JMenuItem iconManagerMenu = new JMenuItem("Icon Manager");
|
||||
iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12));
|
||||
iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
|
||||
viewMenu.add(iconManagerMenu);
|
||||
|
||||
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
|
||||
recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12));
|
||||
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
|
||||
viewMenu.add(recordingViewerMenu);
|
||||
|
||||
JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures");
|
||||
viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
|
||||
viewScreenCapturesMenu.addActionListener(arg0 -> {
|
||||
File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile();
|
||||
try
|
||||
{
|
||||
Desktop.getDesktop().open(screenCapturesDirectory);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't open file explorer");
|
||||
|
||||
JOptionPane.showMessageDialog(this, "Can't launch file explorer - files are located at: " + screenCapturesDirectory,
|
||||
"Can't launch file explorer", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
viewMenu.add(viewScreenCapturesMenu);
|
||||
|
||||
JMenuItem preferencesItem = new JMenuItem("User Preferences");
|
||||
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
|
||||
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
|
||||
viewMenu.add(preferencesItem);
|
||||
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new TunersMenu());
|
||||
viewMenu.add(new JSeparator());
|
||||
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel, mUserPreferences));
|
||||
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
|
||||
viewMenu.add(new BroadcastStatusVisibleMenuItem());
|
||||
viewMenu.add(new ResourceStatusVisibleMenuItem());
|
||||
|
||||
menuBar.add(viewMenu);
|
||||
|
||||
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
|
||||
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
|
||||
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
|
||||
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
|
||||
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
|
||||
screenCaptureItem.addActionListener(arg0 -> {
|
||||
try
|
||||
{
|
||||
Robot robot = new Robot();
|
||||
|
||||
final BufferedImage image = robot.createScreenCapture(getBounds());
|
||||
|
||||
String filename = TimeStamp.getTimeStamp("_") + "_screen_capture.png";
|
||||
|
||||
final Path captureFile = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().resolve(filename);
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
try
|
||||
{
|
||||
ImageIO.write(image, "png", captureFile.toFile());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Couldn't write screen capture to file [" + captureFile.toString() + "]", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(AWTException e)
|
||||
{
|
||||
mLog.error("Exception while taking screen capture", e);
|
||||
}
|
||||
});
|
||||
|
||||
menuBar.add(screenCaptureItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title bar with the tuner name
|
||||
* @param tunerName optional
|
||||
*/
|
||||
private void updateTitle(String tunerName)
|
||||
{
|
||||
if(tunerName != null)
|
||||
{
|
||||
setTitle(mTitle + " - " + tunerName);
|
||||
}
|
||||
else
|
||||
{
|
||||
setTitle(mTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(TunerEvent event)
|
||||
{
|
||||
switch(event.getEvent())
|
||||
{
|
||||
case REQUEST_MAIN_SPECTRAL_DISPLAY:
|
||||
updateTitle(event.getTuner().getPreferredName());
|
||||
break;
|
||||
case REQUEST_CLEAR_MAIN_SPECTRAL_DISPLAY:
|
||||
updateTitle(null);
|
||||
break;
|
||||
case NOTIFICATION_SHUTTING_DOWN:
|
||||
Tuner currentTuner = mSpectralPanel.getTuner();
|
||||
|
||||
if(event.hasTuner() && event.getTuner().equals(currentTuner) || currentTuner == null)
|
||||
{
|
||||
updateTitle(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy constructor for broadcast status panel
|
||||
*/
|
||||
private BroadcastStatusPanel getBroadcastStatusPanel()
|
||||
{
|
||||
return mBroadcastStatusPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the broadcast channels status panel at the bottom of the controller panel
|
||||
*/
|
||||
private void toggleBroadcastStatusPanelVisibility()
|
||||
{
|
||||
mBroadcastStatusVisible = !mBroadcastStatusVisible;
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
if(mBroadcastStatusVisible)
|
||||
{
|
||||
mSplitPane.add(getBroadcastStatusPanel());
|
||||
}
|
||||
else
|
||||
{
|
||||
mSplitPane.remove(getBroadcastStatusPanel());
|
||||
}
|
||||
|
||||
revalidate();
|
||||
});
|
||||
|
||||
mPreferences.putBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy constructor for resource status panel
|
||||
*/
|
||||
private JFXPanel getResourceStatusPanel()
|
||||
{
|
||||
|
||||
if(mResourceStatusPanel == null)
|
||||
{
|
||||
mResourceStatusPanel = mJavaFxWindowManager.getStatusPanel(mResourceMonitor);
|
||||
}
|
||||
|
||||
return mResourceStatusPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the resource status panel at the bottom of the main UI window
|
||||
*/
|
||||
private void toggleResourceStatusPanelVisibility()
|
||||
{
|
||||
mResourceStatusVisible = !mResourceStatusVisible;
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
if(mResourceStatusVisible)
|
||||
{
|
||||
add(getResourceStatusPanel(), "span,growx");
|
||||
}
|
||||
else
|
||||
{
|
||||
remove(getResourceStatusPanel());
|
||||
}
|
||||
|
||||
revalidate();
|
||||
});
|
||||
|
||||
mPreferences.putBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, mResourceStatusVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles visibility of the Now Playing channel details panel
|
||||
*/
|
||||
private void toggleNowPlayingDetailsPanelVisibility()
|
||||
{
|
||||
mNowPlayingDetailsVisible = !mNowPlayingDetailsVisible;
|
||||
mControllerPanel.getNowPlayingPanel().setDetailTabsVisible(mNowPlayingDetailsVisible);
|
||||
mPreferences.putBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, mNowPlayingDetailsVisible);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs shutdown operations. This is triggered by the shutdown monitor's windowClosing() method
|
||||
*/
|
||||
private void shutdown()
|
||||
{
|
||||
mLog.info("SDRTrunk UI shutdown started ...");
|
||||
mUserPreferences.getSwingPreference().setLocation(WINDOW_FRAME_IDENTIFIER, getLocation());
|
||||
mUserPreferences.getSwingPreference().setDimension(WINDOW_FRAME_IDENTIFIER, getSize());
|
||||
mUserPreferences.getSwingPreference().setMaximized(WINDOW_FRAME_IDENTIFIER,
|
||||
(getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH);
|
||||
mUserPreferences.getSwingPreference().setDimension(SPECTRAL_PANEL_IDENTIFIER, mSpectralPanel.getSize());
|
||||
mUserPreferences.getSwingPreference().setDimension(CONTROLLER_PANEL_IDENTIFIER, mControllerPanel.getSize());
|
||||
mLog.info("Stopping JavaFX window manager ...");
|
||||
mJavaFxWindowManager.shutdown();
|
||||
mLog.info("Stopping resource monitor ...");
|
||||
mResourceMonitor.stop();
|
||||
mLog.info("Stopping spectral display ...");
|
||||
mSpectralPanel.clearTuner();
|
||||
}
|
||||
|
||||
/**
|
||||
* Window adapter to receive window closing event to initiate shutdown.
|
||||
*/
|
||||
public class ShutdownMonitor extends WindowAdapter
|
||||
{
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e)
|
||||
{
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast status panel visible toggle menu item
|
||||
*/
|
||||
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public BroadcastStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Streaming Status");
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleBroadcastStatusPanelVisibility();
|
||||
setSelected(mBroadcastStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource status panel visible toggle menu item
|
||||
*/
|
||||
public class ResourceStatusVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public ResourceStatusVisibleMenuItem()
|
||||
{
|
||||
super("Show Resource Status");
|
||||
setSelected(mResourceStatusVisible);
|
||||
addActionListener(e -> {
|
||||
toggleResourceStatusPanelVisibility();
|
||||
setSelected(mResourceStatusVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now Playing channel details visible toggle menu item
|
||||
*/
|
||||
public class NowPlayingChannelDetailsVisibleMenuItem extends JCheckBoxMenuItem
|
||||
{
|
||||
public NowPlayingChannelDetailsVisibleMenuItem()
|
||||
{
|
||||
super("Show Now Playing Channel Details");
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
addActionListener(e -> {
|
||||
toggleNowPlayingDetailsPanelVisibility();
|
||||
setSelected(mNowPlayingDetailsVisible);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu for selecting tuners for display.
|
||||
*/
|
||||
public class TunersMenu extends JMenu
|
||||
{
|
||||
public TunersMenu()
|
||||
{
|
||||
super("Tuners");
|
||||
|
||||
addMenuListener(new MenuListener()
|
||||
{
|
||||
@Override
|
||||
public void menuSelected(MenuEvent e)
|
||||
{
|
||||
removeAll();
|
||||
|
||||
for(DiscoveredTuner discoveredTuner: mTunerManager.getAvailableTuners())
|
||||
{
|
||||
add(new ShowTunerMenuItem(discoveredTuner.getTuner(), mDiscoveredTunerModel, mUserPreferences));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void menuDeselected(MenuEvent e) { }
|
||||
@Override
|
||||
public void menuCanceled(MenuEvent e) { }
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,710 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzEditor;
|
||||
import io.github.dsheirer.audio.call.CallView;
|
||||
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelsView;
|
||||
import io.github.dsheirer.gui.icon.IconManager;
|
||||
import io.github.dsheirer.gui.playlist.PlaylistEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasBulkEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasItemEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasViewByIdentifierEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.AliasViewByRecordingEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.AMConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.ChannelEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.DMRConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.LTRConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.LTRNetConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.MPT1327ConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.NBFMConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.P25P1ConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.P25P2ConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.PassportConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channel.UnknownConfigurationEditor;
|
||||
import io.github.dsheirer.gui.playlist.channelMap.ChannelMapEditor;
|
||||
import io.github.dsheirer.gui.playlist.manager.PlaylistManagerEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.CountyAgencyEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.CountySystemEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.NationalAgencyEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.SiteEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.StateAgencyEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.StateSystemEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.SystemSiteSelectionEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.SystemTalkgroupSelectionEditor;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.TalkgroupEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.BroadcastifyCallEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.BroadcastifyStreamEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.IcecastHTTPStreamEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.IcecastTCPStreamEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.RdioScannerEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.ShoutcastV1StreamEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.ShoutcastV2StreamEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.StreamAliasSelectionEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.StreamingEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.UnknownStreamEditor;
|
||||
import io.github.dsheirer.gui.preference.DecodeEventViewPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.TalkgroupFormatPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
|
||||
import io.github.dsheirer.gui.preference.application.ApplicationPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.audio.call.CallManagementPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.audio.playback.PlaybackPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.audio.record.RecordPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.audio.retention.RetentionPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.calibration.VectorCalibrationPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
|
||||
import io.github.dsheirer.jmbe.JmbeEditor;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import jiconfont.javafx.IconFontFX;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
|
||||
/**
|
||||
* Spring configuration factory for JavaFX UI components that require dependency injection (@Resource)
|
||||
*
|
||||
* Note: all bean creator methods are also annotated as @Lazy so that Spring doesn't auto-instantiate these beans when
|
||||
* we're running in a headless environment.
|
||||
*/
|
||||
@Configuration
|
||||
public class SpringJavaFxBeanFactory
|
||||
{
|
||||
private JFXPanel mJFXPanel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public SpringJavaFxBeanFactory()
|
||||
{
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
//Initialize the JavafX platform by creating a JFXPanel.
|
||||
createJFXPanel();
|
||||
|
||||
//Register JavaFX icon fonts
|
||||
IconFontFX.register(jiconfont.icons.font_awesome.FontAwesome.getIconFont());
|
||||
IconFontFX.register(jiconfont.icons.elusive.Elusive.getIconFont());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the JavaFX platform by creating a JavaFX panel for Swing application compatibility
|
||||
*/
|
||||
private void createJFXPanel()
|
||||
{
|
||||
if(mJFXPanel == null)
|
||||
{
|
||||
mJFXPanel = new JFXPanel();
|
||||
Platform.setImplicitExit(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call view Panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public CallView getCallViewPanel()
|
||||
{
|
||||
return new CallView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio playback channels view
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AudioPlaybackChannelsView getAudioPlaybackChannelsView()
|
||||
{
|
||||
return new AudioPlaybackChannelsView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias Item Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AliasItemEditor getAliasItemEditor()
|
||||
{
|
||||
return new AliasItemEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias Bulk Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AliasBulkEditor getAliasBulkEditor()
|
||||
{
|
||||
return new AliasBulkEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias Configuration Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AliasConfigurationEditor getAliasConfigurationEditor()
|
||||
{
|
||||
return new AliasConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AliasEditor getAliasEditor()
|
||||
{
|
||||
return new AliasEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias View By Identifier Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AliasViewByIdentifierEditor getAliasViewByIdentifierEditor()
|
||||
{
|
||||
return new AliasViewByIdentifierEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ChannelEditor getChannelEditor()
|
||||
{
|
||||
return new ChannelEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Playlist Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public PlaylistEditor getPlaylistEditor()
|
||||
{
|
||||
return new PlaylistEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Playlist Manager Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public PlaylistManagerEditor getPlaylistManagerEditor()
|
||||
{
|
||||
return new PlaylistManagerEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Radio Reference Editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public RadioReferenceEditor getRadioReferenceEditor()
|
||||
{
|
||||
return new RadioReferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream alias selection editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public StreamAliasSelectionEditor getStreamAliasSelectionEditor()
|
||||
{
|
||||
return new StreamAliasSelectionEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Streaming editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public StreamingEditor getStreamingEditor()
|
||||
{
|
||||
return new StreamingEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* AM channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AMConfigurationEditor getAMConfigurationEditor()
|
||||
{
|
||||
return new AMConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* NBFM channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public NBFMConfigurationEditor getNBFMConfigurationEditor()
|
||||
{
|
||||
return new NBFMConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* DMR channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public DMRConfigurationEditor getDMRConfigurationEditor()
|
||||
{
|
||||
return new DMRConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* LTR channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public LTRConfigurationEditor getLTRConfigurationEditor()
|
||||
{
|
||||
return new LTRConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* LTR-Net channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public LTRNetConfigurationEditor getLTRNetConfigurationEditor()
|
||||
{
|
||||
return new LTRNetConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* MPT-1327 channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public MPT1327ConfigurationEditor getMPT1327ConfigurationEditor()
|
||||
{
|
||||
return new MPT1327ConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 1 channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public P25P1ConfigurationEditor getP25P1ConfigurationEditor()
|
||||
{
|
||||
return new P25P1ConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* P25 Phase 2 channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public P25P2ConfigurationEditor getP25P2ConfigurationEditor()
|
||||
{
|
||||
return new P25P2ConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Passport channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public PassportConfigurationEditor getPassportConfigurationEditor()
|
||||
{
|
||||
return new PassportConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown channel configuration editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public UnknownConfigurationEditor getUnknownConfigurationEditor()
|
||||
{
|
||||
return new UnknownConfigurationEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcastify stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public BroadcastifyStreamEditor getBroadcastifyStreamEditor()
|
||||
{
|
||||
return new BroadcastifyStreamEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcastify calls stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public BroadcastifyCallEditor getBroadcastifyCallEditor()
|
||||
{
|
||||
return new BroadcastifyCallEditor();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* OpenMHZ stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public OpenMHzEditor getOpenMhzEditor()
|
||||
{
|
||||
return new OpenMHzEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rdio scanner stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public RdioScannerEditor getRdioScannerEditor()
|
||||
{
|
||||
return new RdioScannerEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Icecast TCP stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public IcecastTCPStreamEditor getIcecastTcpStreamEditor()
|
||||
{
|
||||
return new IcecastTCPStreamEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Icecast HTTP stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public IcecastHTTPStreamEditor getIcecastHTTPStreamEditor()
|
||||
{
|
||||
return new IcecastHTTPStreamEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shoutcast V1 stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ShoutcastV1StreamEditor getShoutcastV1StreamEditor()
|
||||
{
|
||||
return new ShoutcastV1StreamEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shoutcast V1 stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ShoutcastV2StreamEditor getShoutcastV2StreamEditor()
|
||||
{
|
||||
return new ShoutcastV2StreamEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unknown stream editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public UnknownStreamEditor getUnknownStreamEditor()
|
||||
{
|
||||
return new UnknownStreamEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* County agency editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public CountyAgencyEditor getCountyAgencyEditor()
|
||||
{
|
||||
return new CountyAgencyEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* County system editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public CountySystemEditor getCountySystemEditor()
|
||||
{
|
||||
return new CountySystemEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* State agency editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public StateAgencyEditor getStateAgencyEditor()
|
||||
{
|
||||
return new StateAgencyEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* State system editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public StateSystemEditor getStateSystemEditor()
|
||||
{
|
||||
return new StateSystemEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* National agency editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public NationalAgencyEditor getNationalAgencyEditor()
|
||||
{
|
||||
return new NationalAgencyEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* System talkgroup selection editor
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public SystemTalkgroupSelectionEditor getSystemTalkgroupSelectionEditor()
|
||||
{
|
||||
return new SystemTalkgroupSelectionEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Site editor
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public SiteEditor getSiteEditor()
|
||||
{
|
||||
return new SiteEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup editor
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public TalkgroupEditor getTalkgroupEditor()
|
||||
{
|
||||
return new TalkgroupEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* System site selection editor
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public SystemSiteSelectionEditor getSystemSiteSelectionEditor()
|
||||
{
|
||||
return new SystemSiteSelectionEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel map editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ChannelMapEditor getChannelMapEditor()
|
||||
{
|
||||
return new ChannelMapEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel map editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public IconManager getIconManager()
|
||||
{
|
||||
return new IconManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* User preferences editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public UserPreferencesEditor getUserPreferencesEditor()
|
||||
{
|
||||
return new UserPreferencesEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Jmbe editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public JmbeEditor getJmbeEditor()
|
||||
{
|
||||
return new JmbeEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias view-by recording editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AliasViewByRecordingEditor getAliasViewByRecordingEditor()
|
||||
{
|
||||
return new AliasViewByRecordingEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Application preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ApplicationPreferenceEditor getApplicationPreferenceEditor()
|
||||
{
|
||||
return new ApplicationPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call management preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public CallManagementPreferenceEditor getCallManagementPreferenceEditor()
|
||||
{
|
||||
return new CallManagementPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public PlaybackPreferenceEditor getPlaybackPreferenceEditor()
|
||||
{
|
||||
return new PlaybackPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public RecordPreferenceEditor getRecordPreferenceEditor()
|
||||
{
|
||||
return new RecordPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio/call retention preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public RetentionPreferenceEditor getRetentionPreferenceEditor()
|
||||
{
|
||||
return new RetentionPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode event view preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public DecodeEventViewPreferenceEditor getDecodeEventViewPreferenceEditor()
|
||||
{
|
||||
return new DecodeEventViewPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public DirectoryPreferenceEditor getDirectoryPreferenceEditor()
|
||||
{
|
||||
return new DirectoryPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public JmbeLibraryPreferenceEditor getJmbeLibraryPreferenceEditor()
|
||||
{
|
||||
return new JmbeLibraryPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tuner preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public TunerPreferenceEditor getTunerPreferenceEditor()
|
||||
{
|
||||
return new TunerPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Talkgroup format preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public TalkgroupFormatPreferenceEditor getTalkgroupFormatPreferenceEditor()
|
||||
{
|
||||
return new TalkgroupFormatPreferenceEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector calibration preference editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public VectorCalibrationPreferenceEditor getVectorCalibrationPreferenceEditor()
|
||||
{
|
||||
return new VectorCalibrationPreferenceEditor();
|
||||
}
|
||||
}
|
||||
249
src/main/java/io/github/dsheirer/gui/SpringSwingBeanFactory.java
Normal file
249
src/main/java/io/github/dsheirer/gui/SpringSwingBeanFactory.java
Normal file
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
||||
import io.github.dsheirer.audio.playback.AudioPanel;
|
||||
import io.github.dsheirer.channel.details.ChannelDetailPanel;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadataPanel;
|
||||
import io.github.dsheirer.channel.metadata.NowPlayingPanel;
|
||||
import io.github.dsheirer.controller.ControllerPanel;
|
||||
import io.github.dsheirer.gui.power.ChannelPowerPanel;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
|
||||
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerEditor;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerViewPanel;
|
||||
import io.github.dsheirer.spectrum.FrequencyOverlayPanel;
|
||||
import io.github.dsheirer.spectrum.OverlayPanel;
|
||||
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||
import io.github.dsheirer.spectrum.SpectrumFrame;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.WaterfallPanel;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
|
||||
/**
|
||||
* Spring configuration factory for Swing UI components that require dependency injection (@Resource)
|
||||
*
|
||||
* Note: all bean creator methods are also annotated as @Lazy so that Spring doesn't auto-instantiate these beans when
|
||||
* we're running in a headless environment.
|
||||
*/
|
||||
@Configuration
|
||||
public class SpringSwingBeanFactory
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public SpringSwingBeanFactory()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the primary UI instance when running in non-headless mode.
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public SDRTrunkUI getSDRTrunkUI()
|
||||
{
|
||||
if(GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDRTrunkUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new spectrum frame
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public SpectrumFrame createSpectrumFrame()
|
||||
{
|
||||
return new SpectrumFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the spectral display panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
public SpectralDisplayPanel getSpectralDisplayPanel()
|
||||
{
|
||||
return new SpectralDisplayPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the spectrum panel
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public SpectrumPanel getSpectrumPanel()
|
||||
{
|
||||
return new SpectrumPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the waterfall panel
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public WaterfallPanel getWaterfallPanel()
|
||||
{
|
||||
return new WaterfallPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the overlay panel
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public OverlayPanel getOverlayPanel()
|
||||
{
|
||||
return new OverlayPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs the frequency overlay panel
|
||||
*
|
||||
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
|
||||
*/
|
||||
@Bean
|
||||
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
|
||||
@Lazy
|
||||
public FrequencyOverlayPanel getFrequencyOverlayPanel()
|
||||
{
|
||||
return new FrequencyOverlayPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a broadcast (ie streaming) status panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public BroadcastStatusPanel getBroadcastStatusPanel()
|
||||
{
|
||||
return new BroadcastStatusPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the controller panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ControllerPanel controllerPanel()
|
||||
{
|
||||
return new ControllerPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the now playing panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public NowPlayingPanel getNowPlayingPanel()
|
||||
{
|
||||
return new NowPlayingPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the channel metadata panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ChannelMetadataPanel getChannelMetadataPanel()
|
||||
{
|
||||
return new ChannelMetadataPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the channel detail panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ChannelDetailPanel getChannelDetailPanel()
|
||||
{
|
||||
return new ChannelDetailPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the channel power panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public ChannelPowerPanel getChannelPowerPanel()
|
||||
{
|
||||
return new ChannelPowerPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the decode event panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public DecodeEventPanel getDecodeEventPanel()
|
||||
{
|
||||
return new DecodeEventPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public AudioPanel getAudioPanel()
|
||||
{
|
||||
return new AudioPanel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates discovered tuner editor
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public DiscoveredTunerEditor getDiscoveredTunerEditor()
|
||||
{
|
||||
return new DiscoveredTunerEditor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates tuner view panel
|
||||
*/
|
||||
@Bean
|
||||
@Lazy
|
||||
public TunerViewPanel getTunerViewPanel()
|
||||
{
|
||||
return new TunerViewPanel();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.calls;
|
||||
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* Call audio panel for managing call audio playback.
|
||||
*/
|
||||
public class CallAudioPanel extends VBox
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -35,6 +35,13 @@ import io.github.dsheirer.spectrum.ComplexDftProcessor;
|
||||
import io.github.dsheirer.spectrum.DFTSize;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -47,13 +54,6 @@ import javax.swing.JToggleButton;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
@@ -316,7 +316,7 @@ public class ChannelizerViewer extends JFrame
|
||||
public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(28);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
@@ -354,7 +354,7 @@ public class ChannelizerViewer extends JFrame
|
||||
public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(32);
|
||||
add(mSpectrumPanel, "span");
|
||||
|
||||
@@ -435,7 +435,7 @@ public class ChannelizerViewer extends JFrame
|
||||
{
|
||||
mSource = source;
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(32);
|
||||
add(mSpectrumPanel, "span");
|
||||
add(new JLabel("Index:" + index));
|
||||
|
||||
@@ -333,7 +333,7 @@ public class ChannelizerViewer2 extends JFrame
|
||||
public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(16);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
@@ -371,7 +371,7 @@ public class ChannelizerViewer2 extends JFrame
|
||||
public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill][grow,fill][grow,fill]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(16);
|
||||
add(mSpectrumPanel, "span");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -36,6 +36,13 @@ import io.github.dsheirer.spectrum.ComplexDftProcessor;
|
||||
import io.github.dsheirer.spectrum.DFTSize;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -48,13 +55,6 @@ import javax.swing.JToggleButton;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class HeterodyneChannelizerViewer extends JFrame
|
||||
{
|
||||
@@ -315,7 +315,7 @@ public class HeterodyneChannelizerViewer extends JFrame
|
||||
public PrimarySpectrumPanel(SettingsManager settingsManager, double sampleRate)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(28);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
@@ -353,7 +353,7 @@ public class HeterodyneChannelizerViewer extends JFrame
|
||||
public ChannelPanel(SettingsManager settingsManager, double sampleRate, long frequency, int bandwidth, boolean enableLogging)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(32);
|
||||
add(mSpectrumPanel, "span");
|
||||
|
||||
@@ -435,7 +435,7 @@ public class HeterodyneChannelizerViewer extends JFrame
|
||||
{
|
||||
mSource = source;
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[center,grow,fill][]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(32);
|
||||
add(mSpectrumPanel, "span");
|
||||
add(new JLabel("Index:" + index));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -150,7 +150,7 @@ public class SynthesizerViewer extends JFrame
|
||||
public PrimarySpectrumPanel(SettingsManager settingsManager)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(16);
|
||||
add(mSpectrumPanel);
|
||||
|
||||
@@ -181,7 +181,7 @@ public class SynthesizerViewer extends JFrame
|
||||
public ChannelPanel(SettingsManager settingsManager, ChannelControlPanel channelControlPanel)
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[grow,fill][]"));
|
||||
mSpectrumPanel = new SpectrumPanel(settingsManager);
|
||||
mSpectrumPanel = new SpectrumPanel();
|
||||
mSpectrumPanel.setSampleSize(16);
|
||||
add(mSpectrumPanel, "wrap");
|
||||
add(channelControlPanel);
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.control;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.util.Callback;
|
||||
|
||||
/**
|
||||
* Table cell factory that formats a long timestamp column value as a formatted date/time stamp.
|
||||
* @param <T> is the table row entity type
|
||||
*/
|
||||
public class TimestampTableCellFactory<T> implements Callback<TableColumn<T, Long>, TableCell<T, Long>>
|
||||
{
|
||||
private SimpleDateFormat mSDF = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
|
||||
|
||||
@Override
|
||||
public TableCell<T, Long> call(TableColumn<T, Long> param)
|
||||
{
|
||||
return new TableCell<>()
|
||||
{
|
||||
@Override
|
||||
protected void updateItem(Long item, boolean empty)
|
||||
{
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if(empty)
|
||||
{
|
||||
setText(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(item instanceof Long l)
|
||||
{
|
||||
String a = mSDF.format(l);
|
||||
setText(a);
|
||||
}
|
||||
else
|
||||
{
|
||||
setText("Not a Long: " + item.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -22,6 +22,11 @@ package io.github.dsheirer.gui.icon;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.icon.Icon;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.geometry.HPos;
|
||||
@@ -48,17 +53,16 @@ import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* GUI manager/editor for standard and custom icons used in sdrtrunk
|
||||
*/
|
||||
public class IconManager extends Editor<Icon>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(IconManager.class);
|
||||
|
||||
@Resource
|
||||
private IconModel mIconModel;
|
||||
|
||||
private TableView<Icon> mIconTableView;
|
||||
private Button mAddButton;
|
||||
private Button mEditButton;
|
||||
@@ -78,9 +82,13 @@ public class IconManager extends Editor<Icon>
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public IconManager(IconModel iconModel)
|
||||
public IconManager()
|
||||
{
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
mIconModel = iconModel;
|
||||
getChildren().addAll(getAccordion());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.javafx.table;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.cell.CheckBoxTableCell;
|
||||
import javafx.scene.control.cell.PropertyValueFactory;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
/**
|
||||
* Editor for managing table column ordering and visibility.
|
||||
*/
|
||||
public class TableColumnEditor extends VBox
|
||||
{
|
||||
private TableView mTableView;
|
||||
private TableViewColumnController mTableViewColumnController;
|
||||
private TableView mColumnsTableView;
|
||||
private Button mCloseButton;
|
||||
private Button mOrderUpButton;
|
||||
private Button mOrderDownButton;
|
||||
private Button mResetColumnsButton;
|
||||
private BooleanProperty mClosed = new SimpleBooleanProperty();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param tableView to be configured/managed
|
||||
*/
|
||||
public TableColumnEditor(TableView tableView, TableViewColumnController monitor)
|
||||
{
|
||||
mTableView = tableView;
|
||||
mTableViewColumnController = monitor;
|
||||
setPadding(new Insets(10));
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(10);
|
||||
gridPane.setVgap(5);
|
||||
int row = 0;
|
||||
|
||||
Label infoLabel = new Label("Configure Columns");
|
||||
GridPane.setConstraints(infoLabel, 0, row);
|
||||
gridPane.getChildren().add(infoLabel);
|
||||
|
||||
GridPane.setConstraints(getCloseButton(), 1, row);
|
||||
gridPane.getChildren().add(getCloseButton());
|
||||
|
||||
row++;
|
||||
|
||||
GridPane.setConstraints(getColumnsTableView(), 0, row, 1, 6);
|
||||
gridPane.getChildren().add(getColumnsTableView());
|
||||
|
||||
row += 2;
|
||||
|
||||
GridPane.setConstraints(getOrderUpButton(), 1, row);
|
||||
gridPane.getChildren().add(getOrderUpButton());
|
||||
|
||||
row++;
|
||||
|
||||
GridPane.setConstraints(getOrderDownButton(), 1, row);
|
||||
gridPane.getChildren().add(getOrderDownButton());
|
||||
|
||||
row++;
|
||||
GridPane.setConstraints(getResetColumnsButton(), 1, row);
|
||||
gridPane.getChildren().add(getResetColumnsButton());
|
||||
|
||||
ColumnConstraints cc1 = new ColumnConstraints();
|
||||
cc1.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints cc2 = new ColumnConstraints();
|
||||
cc2.setHgrow(Priority.NEVER);
|
||||
cc2.setPrefWidth(60);
|
||||
cc2.setMinWidth(60);
|
||||
gridPane.getColumnConstraints().addAll(cc1, cc2);
|
||||
|
||||
VBox.setVgrow(gridPane, Priority.ALWAYS);
|
||||
getChildren().add(gridPane);
|
||||
}
|
||||
|
||||
private TableView<TableColumn> getColumnsTableView()
|
||||
{
|
||||
if(mColumnsTableView == null)
|
||||
{
|
||||
mColumnsTableView = new TableView(mTableView.getColumns());
|
||||
mColumnsTableView.setEditable(true);
|
||||
|
||||
TableColumn textColumn = new TableColumn("Column");
|
||||
textColumn.setEditable(false);
|
||||
textColumn.setCellValueFactory(new PropertyValueFactory<>("text"));
|
||||
mColumnsTableView.getColumns().add(textColumn);
|
||||
|
||||
TableColumn<TableColumn,Boolean> visibleColumn = new TableColumn("Visible");
|
||||
visibleColumn.setCellValueFactory(new PropertyValueFactory<>("visible"));
|
||||
visibleColumn.setCellFactory(CheckBoxTableCell.forTableColumn(visibleColumn));
|
||||
visibleColumn.setEditable(true);
|
||||
mColumnsTableView.getColumns().add(visibleColumn);
|
||||
|
||||
mColumnsTableView.getSelectionModel().selectedItemProperty().addListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue observable, Object oldValue, Object newValue)
|
||||
{
|
||||
//If monitored table has only a single column keep both buttons disabled.
|
||||
if(mTableView.getColumns().size() <= 1)
|
||||
{
|
||||
getOrderDownButton().setDisable(true);
|
||||
getOrderUpButton().setDisable(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if(newValue == null)
|
||||
{
|
||||
getOrderDownButton().setDisable(true);
|
||||
getOrderUpButton().setDisable(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mColumnsTableView.getSelectionModel().isSelected(0))
|
||||
{
|
||||
getOrderDownButton().setDisable(false);
|
||||
getOrderUpButton().setDisable(true);
|
||||
}
|
||||
else if(mColumnsTableView.getSelectionModel().isSelected(mTableView.getColumns().size() - 1))
|
||||
{
|
||||
getOrderDownButton().setDisable(true);
|
||||
getOrderUpButton().setDisable(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
getOrderDownButton().setDisable(false);
|
||||
getOrderUpButton().setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mColumnsTableView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close button
|
||||
* @return button
|
||||
*/
|
||||
private Button getCloseButton()
|
||||
{
|
||||
if(mCloseButton == null)
|
||||
{
|
||||
mCloseButton = new Button("Close");
|
||||
mCloseButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mCloseButton.setOnAction(event -> closedProperty().set(true));
|
||||
}
|
||||
|
||||
return mCloseButton;
|
||||
}
|
||||
|
||||
private Button getOrderUpButton()
|
||||
{
|
||||
if(mOrderUpButton == null)
|
||||
{
|
||||
mOrderUpButton = new Button("Up");
|
||||
mOrderUpButton.setDisable(true);
|
||||
mOrderUpButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mOrderUpButton.setOnAction(event -> {
|
||||
if(mColumnsTableView.getSelectionModel().selectedItemProperty().get() != null)
|
||||
{
|
||||
TableColumn selected = (TableColumn)mColumnsTableView.getSelectionModel().getSelectedItem();
|
||||
int currentIndex = mTableView.getColumns().indexOf(selected);
|
||||
mTableView.getColumns().remove(selected);
|
||||
mTableView.getColumns().add(currentIndex - 1, selected);
|
||||
//Reselect the column since it moved and lost the selection
|
||||
mColumnsTableView.getSelectionModel().select(selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mOrderUpButton;
|
||||
}
|
||||
|
||||
private Button getOrderDownButton()
|
||||
{
|
||||
if(mOrderDownButton == null)
|
||||
{
|
||||
mOrderDownButton = new Button("Down");
|
||||
mOrderDownButton.setDisable(true);
|
||||
mOrderDownButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mOrderDownButton.setOnAction(event -> {
|
||||
if(mColumnsTableView.getSelectionModel().selectedItemProperty().get() != null)
|
||||
{
|
||||
TableColumn selected = (TableColumn)mColumnsTableView.getSelectionModel().getSelectedItem();
|
||||
int currentIndex = mTableView.getColumns().indexOf(selected);
|
||||
mTableView.getColumns().remove(selected);
|
||||
mTableView.getColumns().add(currentIndex + 1, selected);
|
||||
//Reselect the column since it moved and lost the selection
|
||||
mColumnsTableView.getSelectionModel().select(selected);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mOrderDownButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the columns to default state.
|
||||
* @return button to reset
|
||||
*/
|
||||
private Button getResetColumnsButton()
|
||||
{
|
||||
if(mResetColumnsButton == null)
|
||||
{
|
||||
mResetColumnsButton = new Button("Reset");
|
||||
mResetColumnsButton.setMaxWidth(Double.MAX_VALUE);
|
||||
mResetColumnsButton.setOnAction(event -> mTableViewColumnController.reset());
|
||||
}
|
||||
|
||||
return mResetColumnsButton;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable property to monitor for when the user clicks the close button.
|
||||
*/
|
||||
public BooleanProperty closedProperty()
|
||||
{
|
||||
return mClosed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.javafx.table;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
|
||||
/**
|
||||
* Captures the state of table columns.
|
||||
*/
|
||||
public class TableState
|
||||
{
|
||||
private List<ColumnState> mColumns = new ArrayList<>();
|
||||
private List<SortedColumn> mSort = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public TableState()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to create a table state from a list of table columns
|
||||
* @param columns to capture state
|
||||
* @return table state instance
|
||||
*/
|
||||
public static TableState create(TableView tableView)
|
||||
{
|
||||
TableState tableState = new TableState();
|
||||
|
||||
List<TableColumn> columns = tableView.getColumns();
|
||||
for(TableColumn column: columns)
|
||||
{
|
||||
if(column.getId() == null || column.getId().isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException("TableColumns must have a non-null, non-empty ID value.");
|
||||
}
|
||||
|
||||
tableState.addColumn(new ColumnState(column.getId(), column.isVisible(), (int)column.getWidth()));
|
||||
}
|
||||
|
||||
List<TableColumn> sortedColumns = tableView.getSortOrder();
|
||||
for(TableColumn sortedColumn: sortedColumns)
|
||||
{
|
||||
if(sortedColumn.getId() == null || sortedColumn.getId().isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException("TableColumns must have a non-null, non-empty ID value.");
|
||||
}
|
||||
|
||||
tableState.addSort(new SortedColumn(sortedColumn.getId(), sortedColumn.getSortType()));
|
||||
}
|
||||
|
||||
return tableState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Column states.
|
||||
* @return column states
|
||||
*/
|
||||
public List<ColumnState> getColumns()
|
||||
{
|
||||
return mColumns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a column state
|
||||
* @param columnState to add
|
||||
*/
|
||||
public void addColumn(ColumnState columnState)
|
||||
{
|
||||
mColumns.add(columnState);
|
||||
}
|
||||
|
||||
/**
|
||||
* List of sorted columns
|
||||
* @return sorted columns
|
||||
*/
|
||||
public List<SortedColumn> getSort()
|
||||
{
|
||||
return mSort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the sorted column
|
||||
* @param sortedColumn to add
|
||||
*/
|
||||
public void addSort(SortedColumn sortedColumn)
|
||||
{
|
||||
mSort.add(sortedColumn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column state. Note: arg/variable names are abbreviated for brevity in json representation.
|
||||
* @param id for the column
|
||||
* @param v visible status
|
||||
* @param w width of the column
|
||||
*/
|
||||
public record ColumnState(String id, boolean v, int w) {};
|
||||
|
||||
/**
|
||||
* Sorted column
|
||||
* @param id of the column
|
||||
* @param sortType ascending or descending
|
||||
*/
|
||||
public record SortedColumn(String id, TableColumn.SortType sortType) {};
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.javafx.table;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.prefs.Preferences;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Utility to monitor a JavaFX TableView's column ordering, sizes and visibiltiy and restore those settings on startup.
|
||||
*/
|
||||
public class TableViewColumnController implements ListChangeListener
|
||||
{
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TableViewColumnController.class);
|
||||
private final Preferences mPreferences = Preferences.userNodeForPackage(TableViewColumnController.class);
|
||||
private static final String PREFERENCE_KEY_TABLE_STATE = ".table.state";
|
||||
private String mPreferencePrefix;
|
||||
private TableView mTableView;
|
||||
private TableState mDefaultTableState;
|
||||
private ColumnVisibilityMonitor mVisibilityMonitor = new ColumnVisibilityMonitor();
|
||||
private ColumnWidthMonitor mWidthMonitor = new ColumnWidthMonitor();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param tableView to be monitor and control the column settings
|
||||
* @param preferencePrefix to use for storing the table state in preferences, must be non-null/non-empty/unique.
|
||||
*/
|
||||
public TableViewColumnController(TableView tableView, String preferencePrefix)
|
||||
{
|
||||
if(preferencePrefix == null || preferencePrefix.isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException("Preference prefix [" + preferencePrefix + "] must be non-null & non-empty");
|
||||
}
|
||||
|
||||
mTableView = tableView;
|
||||
mPreferencePrefix = preferencePrefix;
|
||||
mDefaultTableState = TableState.create(mTableView);
|
||||
|
||||
TableState storedTableState = getStoredTableState();
|
||||
|
||||
if(storedTableState != null)
|
||||
{
|
||||
apply(storedTableState);
|
||||
}
|
||||
|
||||
//Register listeners on the table and the individual columns
|
||||
mTableView.getColumns().addListener(this);
|
||||
mTableView.getSortOrder().addListener(this);
|
||||
|
||||
ObservableList<TableColumn> columns = mTableView.getColumns();
|
||||
for(TableColumn tableColumn: columns)
|
||||
{
|
||||
tableColumn.visibleProperty().addListener(mVisibilityMonitor);
|
||||
tableColumn.widthProperty().addListener(mWidthMonitor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the table columns to the default state
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
apply(mDefaultTableState);
|
||||
|
||||
//Clear the preference since we're at default state
|
||||
mPreferences.remove(mPreferencePrefix + PREFERENCE_KEY_TABLE_STATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the table state to the currently monitored table view.
|
||||
* @param tableState to apply.
|
||||
*/
|
||||
private void apply(TableState tableState)
|
||||
{
|
||||
if(tableState != null)
|
||||
{
|
||||
if(tableState.getColumns().size() != mTableView.getColumns().size())
|
||||
{
|
||||
LOG.error("Unable to restore tableview column state - column size mismatch");
|
||||
return;
|
||||
}
|
||||
|
||||
//Remove the columns from the table and re-add them according to the stored ordering.
|
||||
List<TableColumn> columns = new ArrayList<>(mTableView.getColumns());
|
||||
mTableView.getColumns().clear();
|
||||
|
||||
for(TableState.ColumnState columnState: tableState.getColumns())
|
||||
{
|
||||
TableColumn match = null;
|
||||
for(TableColumn tableColumn: columns)
|
||||
{
|
||||
if(tableColumn.getId() != null && tableColumn.getId().equals(columnState.id()))
|
||||
{
|
||||
match = tableColumn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(match != null)
|
||||
{
|
||||
match.setVisible(columnState.v());
|
||||
match.setPrefWidth(columnState.w());
|
||||
mTableView.getColumns().add(match);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.warn("Unable to find matching table column for id [" + columnState.id() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
//If the table names got screwed up in the table state, just add the columns all back in.
|
||||
if(mTableView.getColumns().isEmpty())
|
||||
{
|
||||
mTableView.getColumns().addAll(columns);
|
||||
}
|
||||
|
||||
//Clear and restore the sort ordering
|
||||
mTableView.getSortOrder().clear();
|
||||
for(TableState.SortedColumn sortedColumn: tableState.getSort())
|
||||
{
|
||||
List<TableColumn> tableColumns = mTableView.getColumns();
|
||||
|
||||
for(TableColumn column: tableColumns)
|
||||
{
|
||||
if(sortedColumn.id().equals(column.getId()))
|
||||
{
|
||||
column.setSortType(sortedColumn.sortType());
|
||||
mTableView.getSortOrder().add(column);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores current table state
|
||||
*/
|
||||
private void storeTableState()
|
||||
{
|
||||
TableState tableState = TableState.create(mTableView);
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
try
|
||||
{
|
||||
String json = objectMapper.writeValueAsString(tableState);
|
||||
mPreferences.put(mPreferencePrefix + PREFERENCE_KEY_TABLE_STATE, json);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error creating table state json for table [" + mPreferencePrefix + "]", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored table state
|
||||
* @return stored table state or null if this is the first usage or the state has been reset to default.
|
||||
*/
|
||||
private TableState getStoredTableState()
|
||||
{
|
||||
String json = mPreferences.get(mPreferencePrefix + PREFERENCE_KEY_TABLE_STATE, null);
|
||||
|
||||
if(json != null)
|
||||
{
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
try
|
||||
{
|
||||
return objectMapper.readValue(json.getBytes(), TableState.class);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
LOG.error("Error deserializing table state from json [" + json + "] " + e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the ListChangeListener interface to monitor the table columns for ordering changes.
|
||||
* @param c an object representing the change that was done
|
||||
*/
|
||||
@Override
|
||||
public void onChanged(Change c)
|
||||
{
|
||||
boolean changed = false;
|
||||
|
||||
while(c.next())
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if(changed)
|
||||
{
|
||||
storeTableState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Column visibility monitor
|
||||
*/
|
||||
private class ColumnVisibilityMonitor implements ChangeListener<Boolean>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
|
||||
{
|
||||
storeTableState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Column width monitor
|
||||
*/
|
||||
private class ColumnWidthMonitor implements ChangeListener<Number>
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
|
||||
{
|
||||
storeTableState();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -30,11 +30,14 @@ import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceEditor;
|
||||
import io.github.dsheirer.gui.playlist.streaming.StreamingEditor;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import javafx.application.Platform;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.control.Menu;
|
||||
@@ -54,9 +57,6 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* JavaFX playlist, channels, aliases, streaming and radioreference.com import editor
|
||||
@@ -64,9 +64,17 @@ import java.nio.file.Path;
|
||||
public class PlaylistEditor extends BorderPane
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(PlaylistEditor.class);
|
||||
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private TunerManager mTunerManager;
|
||||
@Resource
|
||||
private AliasEditor mAliasEditor;
|
||||
@Resource
|
||||
private ChannelEditor mChannelEditor;
|
||||
@Resource
|
||||
private PlaylistManagerEditor mPlaylistManagerEditor;
|
||||
@Resource
|
||||
private RadioReferenceEditor mRadioReferenceEditor;
|
||||
@Resource
|
||||
private StreamingEditor mStreamingEditor;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
private MenuBar mMenuBar;
|
||||
private TabPane mTabPane;
|
||||
@@ -75,21 +83,17 @@ public class PlaylistEditor extends BorderPane
|
||||
private Tab mAliasesTab;
|
||||
private Tab mRadioReferenceTab;
|
||||
private Tab mStreamingTab;
|
||||
private AliasEditor mAliasEditor;
|
||||
private ChannelEditor mChannelEditor;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for alias and channel models
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for settings
|
||||
*/
|
||||
public PlaylistEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
|
||||
public PlaylistEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mTunerManager = tunerManager;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
//Throw a new runnable back onto the FX thread to lazy load the editor content after the editor has been
|
||||
//constructed and shown.
|
||||
Platform.runLater(() -> {
|
||||
@@ -221,11 +225,6 @@ public class PlaylistEditor extends BorderPane
|
||||
|
||||
private AliasEditor getAliasEditor()
|
||||
{
|
||||
if(mAliasEditor == null)
|
||||
{
|
||||
mAliasEditor = new AliasEditor(mPlaylistManager, mUserPreferences);
|
||||
}
|
||||
|
||||
return mAliasEditor;
|
||||
}
|
||||
|
||||
@@ -242,11 +241,6 @@ public class PlaylistEditor extends BorderPane
|
||||
|
||||
private ChannelEditor getChannelEditor()
|
||||
{
|
||||
if(mChannelEditor == null)
|
||||
{
|
||||
mChannelEditor = new ChannelEditor(mPlaylistManager, mTunerManager, mUserPreferences);
|
||||
}
|
||||
|
||||
return mChannelEditor;
|
||||
}
|
||||
|
||||
@@ -255,7 +249,7 @@ public class PlaylistEditor extends BorderPane
|
||||
if(mPlaylistsTab == null)
|
||||
{
|
||||
mPlaylistsTab = new Tab("Playlists");
|
||||
mPlaylistsTab.setContent(new PlaylistManagerEditor(mPlaylistManager, mUserPreferences));
|
||||
mPlaylistsTab.setContent(mPlaylistManagerEditor);
|
||||
}
|
||||
|
||||
return mPlaylistsTab;
|
||||
@@ -266,7 +260,15 @@ public class PlaylistEditor extends BorderPane
|
||||
if(mRadioReferenceTab == null)
|
||||
{
|
||||
mRadioReferenceTab = new Tab("Radio Reference");
|
||||
mRadioReferenceTab.setContent(new RadioReferenceEditor(mUserPreferences, mPlaylistManager));
|
||||
mRadioReferenceTab.setContent(mRadioReferenceEditor);
|
||||
|
||||
//Only login once/if the user selects the radio reference tab
|
||||
mRadioReferenceTab.selectedProperty().addListener((observable, oldValue, selected) -> {
|
||||
if(selected)
|
||||
{
|
||||
mRadioReferenceEditor.login();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mRadioReferenceTab;
|
||||
@@ -277,7 +279,7 @@ public class PlaylistEditor extends BorderPane
|
||||
if(mStreamingTab == null)
|
||||
{
|
||||
mStreamingTab = new Tab("Streaming");
|
||||
mStreamingTab.setContent(new StreamingEditor(mPlaylistManager));
|
||||
mStreamingTab.setContent(mStreamingEditor);
|
||||
}
|
||||
|
||||
return mStreamingTab;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -19,13 +19,12 @@
|
||||
|
||||
package io.github.dsheirer.gui.playlist;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.gui.JavaFxWindowManager;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
@@ -45,14 +44,16 @@ public class PlaylistEditorApplication extends Application
|
||||
|
||||
public PlaylistEditorApplication()
|
||||
{
|
||||
AliasModel aliasModel = new AliasModel();
|
||||
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
|
||||
mTunerManager = new TunerManager(mUserPreferences);
|
||||
mTunerManager.start();
|
||||
mPlaylistManager = new PlaylistManager(mUserPreferences, mTunerManager, aliasModel, eventLogManager, new IconModel());
|
||||
}
|
||||
|
||||
mPlaylistManager.init();
|
||||
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences, mTunerManager, mPlaylistManager);
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
EventLogManager eventLogManager = new EventLogManager();
|
||||
mTunerManager = new TunerManager();
|
||||
mTunerManager.start();
|
||||
mPlaylistManager = new PlaylistManager();
|
||||
mJavaFxWindowManager = new JavaFxWindowManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,7 +70,7 @@ public class PlaylistEditorApplication extends Application
|
||||
{
|
||||
if(mPlaylistEditor == null)
|
||||
{
|
||||
mPlaylistEditor = new PlaylistEditor(mPlaylistManager, mTunerManager, mUserPreferences);
|
||||
mPlaylistEditor = new PlaylistEditor();
|
||||
}
|
||||
|
||||
return mPlaylistEditor;
|
||||
|
||||
@@ -23,7 +23,9 @@ import com.google.common.collect.Ordering;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.icon.Icon;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.List;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
@@ -52,7 +54,8 @@ import org.controlsfx.control.ToggleSwitch;
|
||||
*/
|
||||
public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
{
|
||||
private PlaylistManager mPlaylistManager;
|
||||
@Resource
|
||||
private IconModel mIconModel;
|
||||
private Label mEditingLabel;
|
||||
private ColorPicker mColorPicker;
|
||||
private Button mApplyColorButton;
|
||||
@@ -65,20 +68,19 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
private ToggleSwitch mRecordToggleSwitch;
|
||||
private Button mApplyRecordButton;
|
||||
|
||||
private BooleanProperty mChangeInProgressProperty;
|
||||
private BooleanProperty mChangeInProgressProperty = new SimpleBooleanProperty();
|
||||
private ReadOnlyBooleanProperty mChangeInProgressROProperty;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param playlistManager for accessing icon manager
|
||||
*/
|
||||
public AliasBulkEditor(PlaylistManager playlistManager)
|
||||
public AliasBulkEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
|
||||
mChangeInProgressProperty = new SimpleBooleanProperty();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
gridPane.setHgap(10);
|
||||
@@ -280,7 +282,7 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
{
|
||||
mIconNodeComboBox = new ComboBox<>();
|
||||
mIconNodeComboBox.setMaxWidth(Double.MAX_VALUE);
|
||||
mIconNodeComboBox.setItems(new SortedList(mPlaylistManager.getIconModel().iconsProperty(), Ordering.natural()));
|
||||
mIconNodeComboBox.setItems(new SortedList(mIconModel.iconsProperty(), Ordering.natural()));
|
||||
mIconNodeComboBox.setCellFactory(new IconCellFactory());
|
||||
mIconNodeComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) ->
|
||||
@@ -298,25 +300,20 @@ public class AliasBulkEditor extends Editor<List<Alias>>
|
||||
{
|
||||
mApplyIconButton = new Button("Apply");
|
||||
mApplyIconButton.setDisable(true);
|
||||
mApplyIconButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
mApplyIconButton.setOnAction(event -> {
|
||||
startChange();
|
||||
|
||||
Icon icon = getIconNodeComboBox().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(icon != null)
|
||||
{
|
||||
startChange();
|
||||
|
||||
Icon icon = getIconNodeComboBox().getSelectionModel().getSelectedItem();
|
||||
|
||||
if(icon != null)
|
||||
for(Alias alias : getItem())
|
||||
{
|
||||
for(Alias alias : getItem())
|
||||
{
|
||||
alias.setIconName(icon.getName());
|
||||
}
|
||||
alias.setIconName(icon.getName());
|
||||
}
|
||||
|
||||
endChange();
|
||||
}
|
||||
|
||||
endChange();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
import io.github.dsheirer.icon.Icon;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -80,9 +81,13 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AliasConfigurationEditor.class);
|
||||
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
@Resource
|
||||
private IconModel mIconModel;
|
||||
@Resource
|
||||
private AliasItemEditor mAliasItemEditor;
|
||||
@Resource
|
||||
private AliasBulkEditor mAliasBulkEditor;
|
||||
private Editor mCurrentEditor;
|
||||
private TableView<Alias> mAliasTableView;
|
||||
@@ -100,11 +105,13 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
private SortedList<Alias> mAliasSortedList;
|
||||
private AliasPredicate mAliasPredicate;
|
||||
|
||||
public AliasConfigurationEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
public AliasConfigurationEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
VBox leftBox = new VBox();
|
||||
VBox.setVgrow(getAliasTableView(), Priority.ALWAYS);
|
||||
leftBox.getChildren().addAll(getSearchAndListSelectionBox(), getAliasTableView());
|
||||
@@ -209,21 +216,11 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
|
||||
private AliasItemEditor getAliasItemEditor()
|
||||
{
|
||||
if(mAliasItemEditor == null)
|
||||
{
|
||||
mAliasItemEditor = new AliasItemEditor(mPlaylistManager, mUserPreferences);
|
||||
}
|
||||
|
||||
return mAliasItemEditor;
|
||||
}
|
||||
|
||||
private AliasBulkEditor getAliasBulkEditor()
|
||||
{
|
||||
if(mAliasBulkEditor == null)
|
||||
{
|
||||
mAliasBulkEditor = new AliasBulkEditor(mPlaylistManager);
|
||||
}
|
||||
|
||||
return mAliasBulkEditor;
|
||||
}
|
||||
|
||||
@@ -288,7 +285,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
{
|
||||
if(mAliasListNameComboBox == null)
|
||||
{
|
||||
mAliasListNameComboBox = new ComboBox<>(mPlaylistManager.getAliasModel().aliasListNames());
|
||||
mAliasListNameComboBox = new ComboBox<>(mAliasModel.aliasListNames());
|
||||
mAliasListNameComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((ObservableValue<? extends String> observable, String oldValue, String newValue) ->
|
||||
{
|
||||
@@ -337,7 +334,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
if(name != null && !name.isEmpty())
|
||||
{
|
||||
name = name.trim();
|
||||
mPlaylistManager.getAliasModel().addAliasList(name);
|
||||
mAliasModel.addAliasList(name);
|
||||
getAliasListNameComboBox().getSelectionModel().select(name);
|
||||
}
|
||||
});
|
||||
@@ -436,7 +433,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
{
|
||||
if(mAliasFilteredList == null)
|
||||
{
|
||||
mAliasFilteredList = new FilteredList<>(mPlaylistManager.getAliasModel().aliasList(), getAliasPredicate());
|
||||
mAliasFilteredList = new FilteredList<>(mAliasModel.aliasList(), getAliasPredicate());
|
||||
}
|
||||
|
||||
return mAliasFilteredList;
|
||||
@@ -506,7 +503,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
{
|
||||
Alias alias = new Alias("New Alias");
|
||||
alias.setAliasListName(getAliasListNameComboBox().getSelectionModel().getSelectedItem());
|
||||
mPlaylistManager.getAliasModel().addAlias(alias);
|
||||
mAliasModel.addAlias(alias);
|
||||
|
||||
//Queue a select alias action to allow table to update filter predicate and display the alias
|
||||
Platform.runLater(() ->
|
||||
@@ -544,7 +541,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
if(result.get() == ButtonType.YES)
|
||||
{
|
||||
List<Alias> selectedAliases = new ArrayList<>(getAliasTableView().getSelectionModel().getSelectedItems());
|
||||
mPlaylistManager.getAliasModel().removeAliases(selectedAliases);
|
||||
mAliasModel.removeAliases(selectedAliases);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -563,7 +560,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
{
|
||||
Alias original = getAliasTableView().getSelectionModel().getSelectedItem();
|
||||
Alias copy = AliasFactory.shallowCopyOf(original);
|
||||
mPlaylistManager.getAliasModel().addAlias(copy);
|
||||
mAliasModel.addAlias(copy);
|
||||
getAliasTableView().getSelectionModel().clearSelection();
|
||||
getAliasTableView().getSelectionModel().select(copy);
|
||||
getAliasTableView().scrollTo(copy);
|
||||
@@ -587,7 +584,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
emptyItem.setDisable(true);
|
||||
mMoveToAliasButton.getItems().addAll(emptyItem, new SeparatorMenuItem());
|
||||
|
||||
List<String> aliasLists = mPlaylistManager.getAliasModel().getListNames();
|
||||
List<String> aliasLists = mAliasModel.getListNames();
|
||||
|
||||
for(String aliasList : aliasLists)
|
||||
{
|
||||
@@ -614,11 +611,11 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
List<Alias> selectedAliases = new ArrayList<>(getAliasTableView().getSelectionModel().getSelectedItems());
|
||||
for(Alias selected : selectedAliases)
|
||||
{
|
||||
AliasList existing = mPlaylistManager.getAliasModel().getAliasList(selected.getAliasListName());
|
||||
AliasList existing = mAliasModel.getAliasList(selected.getAliasListName());
|
||||
existing.removeAlias(selected);
|
||||
|
||||
selected.setAliasListName(getText());
|
||||
AliasList moveToList = mPlaylistManager.getAliasModel().getAliasList(selected.getAliasListName());
|
||||
AliasList moveToList = mAliasModel.getAliasList(selected.getAliasListName());
|
||||
moveToList.addAlias(selected);
|
||||
}
|
||||
});
|
||||
@@ -830,7 +827,7 @@ public class AliasConfigurationEditor extends SplitPane
|
||||
|
||||
if(alias != null)
|
||||
{
|
||||
Icon icon = mPlaylistManager.getIconModel().getIcon(alias.getIconName());
|
||||
Icon icon = mIconModel.getIcon(alias.getIconName());
|
||||
|
||||
if(icon != null && icon.getFxImage() != null)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -20,9 +20,11 @@
|
||||
package io.github.dsheirer.gui.playlist.alias;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
@@ -32,29 +34,49 @@ import javafx.scene.control.TabPane;
|
||||
*/
|
||||
public class AliasEditor extends TabPane
|
||||
{
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
@Resource
|
||||
private AliasConfigurationEditor mAliasConfigurationEditor;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
@Resource
|
||||
private AliasViewByIdentifierEditor mAliasViewByIdentifierEditor;
|
||||
@Resource
|
||||
private AliasViewByRecordingEditor mAliasViewByRecordingEditor;
|
||||
private Tab mAliasConfigurationTab;
|
||||
private Tab mAliasIdentifierTab;
|
||||
private Tab mAliasRecordingTab;
|
||||
private boolean mAliasListInvalidated = false;
|
||||
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for alias model access
|
||||
* @param userPreferences for settings
|
||||
*/
|
||||
public AliasEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
public AliasEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
setPadding(new Insets(4,0,0,0));
|
||||
setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
|
||||
Tab viewByTab = new Tab("View By:");
|
||||
viewByTab.setDisable(true);
|
||||
getTabs().addAll(viewByTab, getAliasConfigurationTab(), getAliasIdentifierTab(), getAliasRecordingTab());
|
||||
|
||||
//Note if aliases change since we're using a static snapshot
|
||||
mAliasModel.aliasList().addListener((InvalidationListener)observable -> mAliasListInvalidated = true);
|
||||
|
||||
//Only update the alias list when this tab is selected to avoid many slow updates in the background
|
||||
getAliasIdentifierTab().selectedProperty().addListener(observable ->
|
||||
{
|
||||
if (mAliasListInvalidated)
|
||||
{
|
||||
getAliasViewByIdentifierEditor().updateList();
|
||||
mAliasListInvalidated = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,11 +123,6 @@ public class AliasEditor extends TabPane
|
||||
|
||||
private AliasConfigurationEditor getAliasConfigurationEditor()
|
||||
{
|
||||
if(mAliasConfigurationEditor == null)
|
||||
{
|
||||
mAliasConfigurationEditor = new AliasConfigurationEditor(mPlaylistManager, mUserPreferences);
|
||||
}
|
||||
|
||||
return mAliasConfigurationEditor;
|
||||
}
|
||||
|
||||
@@ -122,20 +139,20 @@ public class AliasEditor extends TabPane
|
||||
|
||||
private AliasViewByIdentifierEditor getAliasViewByIdentifierEditor()
|
||||
{
|
||||
if(mAliasViewByIdentifierEditor == null)
|
||||
{
|
||||
mAliasViewByIdentifierEditor = new AliasViewByIdentifierEditor(mPlaylistManager, getAliasIdentifierTab().selectedProperty());
|
||||
}
|
||||
|
||||
return mAliasViewByIdentifierEditor;
|
||||
}
|
||||
|
||||
private AliasViewByRecordingEditor getAliasViewByRecordingEditor()
|
||||
{
|
||||
return mAliasViewByRecordingEditor;
|
||||
}
|
||||
|
||||
private Tab getAliasRecordingTab()
|
||||
{
|
||||
if(mAliasRecordingTab == null)
|
||||
{
|
||||
mAliasRecordingTab = new Tab("Record");
|
||||
mAliasRecordingTab.setContent(new AliasViewByRecordingEditor(mPlaylistManager));
|
||||
mAliasRecordingTab.setContent(getAliasViewByRecordingEditor());
|
||||
}
|
||||
|
||||
return mAliasRecordingTab;
|
||||
|
||||
@@ -20,12 +20,12 @@
|
||||
package io.github.dsheirer.gui.playlist.alias;
|
||||
|
||||
import com.google.common.collect.Ordering;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import impl.org.controlsfx.autocompletion.AutoCompletionTextFieldBinding;
|
||||
import impl.org.controlsfx.autocompletion.SuggestionProvider;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasFactory;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.action.AliasAction;
|
||||
import io.github.dsheirer.alias.action.AliasActionType;
|
||||
import io.github.dsheirer.alias.action.beep.BeepAction;
|
||||
@@ -48,6 +48,7 @@ import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupFormatter;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastModel;
|
||||
import io.github.dsheirer.audio.broadcast.ConfiguredBroadcast;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
@@ -58,11 +59,15 @@ import io.github.dsheirer.gui.playlist.alias.identifier.EmptyIdentifierEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.identifier.IdentifierEditor;
|
||||
import io.github.dsheirer.gui.playlist.alias.identifier.IdentifierEditorFactory;
|
||||
import io.github.dsheirer.icon.Icon;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.preference.IPreferenceUpdateListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -114,11 +119,17 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* Editor for configuring individual aliases
|
||||
*/
|
||||
public class AliasItemEditor extends Editor<Alias>
|
||||
public class AliasItemEditor extends Editor<Alias> implements IPreferenceUpdateListener
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AliasItemEditor.class);
|
||||
|
||||
private PlaylistManager mPlaylistManager;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
@Resource
|
||||
private BroadcastModel mBroadcastModel;
|
||||
@Resource
|
||||
private IconModel mIconModel;
|
||||
@Resource
|
||||
private UserPreferences mUserPreferences;
|
||||
private EditorModificationListener mEditorModificationListener = new EditorModificationListener();
|
||||
private IdentifierEditorModificationListener mIdentifierEditorModificationListener = new IdentifierEditorModificationListener();
|
||||
@@ -161,15 +172,16 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
private EmptyActionEditor mEmptyActionEditor = new EmptyActionEditor();
|
||||
private ActionEditor mActionEditor;
|
||||
|
||||
|
||||
public AliasItemEditor(PlaylistManager playlistManager, UserPreferences userPreferences)
|
||||
public AliasItemEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
mUserPreferences.addUpdateListener(this);
|
||||
//Listen for changes to the stream configurations and refresh the stream lists
|
||||
mPlaylistManager.getBroadcastModel().getConfiguredBroadcasts()
|
||||
.addListener((ListChangeListener<ConfiguredBroadcast>)c -> updateStreamViews());
|
||||
mBroadcastModel.getConfiguredBroadcasts().addListener((ListChangeListener<ConfiguredBroadcast>)c -> updateStreamViews());
|
||||
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
|
||||
@@ -192,8 +204,13 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
getChildren().add(hbox);
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void dispose()
|
||||
{
|
||||
mUserPreferences.removeUpdateListener(this);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@Override
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
if(preferenceType == PreferenceType.TALKGROUP_FORMAT)
|
||||
@@ -251,7 +268,7 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
String iconName = alias.getIconName();
|
||||
if(iconName != null)
|
||||
{
|
||||
icon = mPlaylistManager.getIconModel().getIcon(iconName);
|
||||
icon = mIconModel.getIcon(iconName);
|
||||
}
|
||||
getIconNodeComboBox().getSelectionModel().select(icon);
|
||||
|
||||
@@ -399,7 +416,7 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
}
|
||||
}
|
||||
|
||||
AliasList aliasList = mPlaylistManager.getAliasModel().getAliasList(alias.getAliasListName());
|
||||
AliasList aliasList = mAliasModel.getAliasList(alias.getAliasListName());
|
||||
aliasList.updateAlias(alias);
|
||||
|
||||
//Reset the alias to refresh the editor.
|
||||
@@ -409,12 +426,6 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
}
|
||||
|
||||
private VBox getTitledPanesBox()
|
||||
{
|
||||
if(mTitledPanesBox == null)
|
||||
@@ -819,7 +830,7 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
|
||||
if(getItem() != null)
|
||||
{
|
||||
List<String> availableStreams = mPlaylistManager.getBroadcastModel().getBroadcastConfigurationNames();
|
||||
List<String> availableStreams = mBroadcastModel.getBroadcastConfigurationNames();
|
||||
|
||||
Set<BroadcastChannel> selectedChannels = getItem().getBroadcastChannels();
|
||||
|
||||
@@ -1088,7 +1099,7 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
mIconNodeComboBox = new ComboBox<>();
|
||||
mIconNodeComboBox.setMaxWidth(Double.MAX_VALUE);
|
||||
mIconNodeComboBox.setDisable(true);
|
||||
mIconNodeComboBox.setItems(new SortedList(mPlaylistManager.getIconModel().iconsProperty(), Ordering.natural()));
|
||||
mIconNodeComboBox.setItems(new SortedList(mIconModel.iconsProperty(), Ordering.natural()));
|
||||
mIconNodeComboBox.setCellFactory(new IconCellFactory());
|
||||
mIconNodeComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
|
||||
@@ -1103,14 +1114,14 @@ public class AliasItemEditor extends Editor<Alias>
|
||||
private void refreshAutoCompleteBindings()
|
||||
{
|
||||
getGroupSuggestionProvider().clearSuggestions();
|
||||
getGroupSuggestionProvider().addPossibleSuggestions(mPlaylistManager.getAliasModel().getGroupNames());
|
||||
getGroupSuggestionProvider().addPossibleSuggestions(mAliasModel.getGroupNames());
|
||||
}
|
||||
|
||||
private SuggestionProvider<String> getGroupSuggestionProvider()
|
||||
{
|
||||
if(mGroupSuggestionProvider == null)
|
||||
{
|
||||
mGroupSuggestionProvider = SuggestionProvider.create(mPlaylistManager.getAliasModel().getGroupNames());
|
||||
mGroupSuggestionProvider = SuggestionProvider.create(mAliasModel.getGroupNames());
|
||||
}
|
||||
|
||||
return mGroupSuggestionProvider;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -24,10 +24,13 @@ import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -50,35 +53,24 @@ import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class AliasViewByIdentifierEditor extends VBox
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AliasViewByIdentifierEditor.class);
|
||||
private PlaylistManager mPlaylistManager;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
private ComboBox<String> mAliasListNameComboBox;
|
||||
private ComboBox<AliasIDType> mAliasIDTypeComboBox;
|
||||
private TableView<AliasAndIdentifier> mAliasAndIdentifierTableView;
|
||||
private TableColumn<AliasAndIdentifier,String> mIdentifierColumn;
|
||||
private Button mViewAliasButton;
|
||||
private boolean aliasListInvalidated;
|
||||
|
||||
public AliasViewByIdentifierEditor(PlaylistManager playlistManager, ReadOnlyBooleanProperty tabSelectedProperty)
|
||||
public AliasViewByIdentifierEditor()
|
||||
{
|
||||
aliasListInvalidated = false;
|
||||
mPlaylistManager = playlistManager;
|
||||
//Note if aliases change since we're using a static snapshot
|
||||
mPlaylistManager.getAliasModel().aliasList().addListener((InvalidationListener)observable -> aliasListInvalidated = true);
|
||||
|
||||
//Only update the list when this tab is selected to avoid many slow updates in the background
|
||||
tabSelectedProperty.addListener(observable -> {
|
||||
if (aliasListInvalidated)
|
||||
updateList();
|
||||
});
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setPadding(new Insets(10,10,10,10));
|
||||
gridPane.setVgap(10);
|
||||
@@ -115,14 +107,14 @@ public class AliasViewByIdentifierEditor extends VBox
|
||||
updateList();
|
||||
}
|
||||
|
||||
private void updateList()
|
||||
public void updateList()
|
||||
{
|
||||
getAliasAndIdentifierTableView().getItems().clear();
|
||||
|
||||
String aliasList = getAliasListNameComboBox().getSelectionModel().getSelectedItem();
|
||||
AliasIDType type = getAliasIDTypeComboBox().getSelectionModel().getSelectedItem();
|
||||
|
||||
List<Alias> aliases = mPlaylistManager.getAliasModel().getAliases(aliasList, type);
|
||||
List<Alias> aliases = mAliasModel.getAliases(aliasList, type);
|
||||
|
||||
for(Alias alias: aliases)
|
||||
{
|
||||
@@ -136,8 +128,6 @@ public class AliasViewByIdentifierEditor extends VBox
|
||||
}
|
||||
|
||||
getAliasAndIdentifierTableView().getSortOrder().setAll(mIdentifierColumn);
|
||||
|
||||
aliasListInvalidated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,8 +220,7 @@ public class AliasViewByIdentifierEditor extends VBox
|
||||
if(mAliasListNameComboBox == null)
|
||||
{
|
||||
Predicate<String> filterPredicate = s -> !s.contentEquals(AliasModel.NO_ALIAS_LIST);
|
||||
FilteredList<String> filteredChannelList =
|
||||
new FilteredList<>(mPlaylistManager.getAliasModel().aliasListNames(), filterPredicate);
|
||||
FilteredList<String> filteredChannelList = new FilteredList<>(mAliasModel.aliasListNames(), filterPredicate);
|
||||
mAliasListNameComboBox = new ComboBox<>(filteredChannelList);
|
||||
mAliasListNameComboBox.setPadding(new Insets(0,10,0,0));
|
||||
if(mAliasListNameComboBox.getItems().size() > 0)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 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
|
||||
@@ -21,7 +21,11 @@ package io.github.dsheirer.gui.playlist.alias;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
@@ -44,15 +48,11 @@ import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class AliasViewByRecordingEditor extends VBox
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AliasViewByRecordingEditor.class);
|
||||
|
||||
private PlaylistManager mPlaylistManager;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
private ComboBox<String> mAliasListNameComboBox;
|
||||
private HBox mTopBox;
|
||||
private TextField mSearchField;
|
||||
@@ -67,10 +67,13 @@ public class AliasViewByRecordingEditor extends VBox
|
||||
private AvailablePredicate mNoRecordPredicate;
|
||||
private SelectedPredicate mRecordPredicate;
|
||||
|
||||
public AliasViewByRecordingEditor(PlaylistManager playlistManager)
|
||||
public AliasViewByRecordingEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
VBox buttonBox = new VBox();
|
||||
buttonBox.setMaxHeight(Double.MAX_VALUE);
|
||||
buttonBox.setAlignment(Pos.CENTER);
|
||||
@@ -105,8 +108,7 @@ public class AliasViewByRecordingEditor extends VBox
|
||||
if(mAliasListNameComboBox == null)
|
||||
{
|
||||
Predicate<String> filterPredicate = s -> !s.contentEquals(AliasModel.NO_ALIAS_LIST);
|
||||
FilteredList<String> filteredChannelList =
|
||||
new FilteredList<>(mPlaylistManager.getAliasModel().aliasListNames(), filterPredicate);
|
||||
FilteredList<String> filteredChannelList = new FilteredList<>(mAliasModel.aliasListNames(), filterPredicate);
|
||||
mAliasListNameComboBox = new ComboBox<>(filteredChannelList);
|
||||
mAliasListNameComboBox.setPadding(new Insets(0,10,0,0));
|
||||
mAliasListNameComboBox.getSelectionModel().selectedItemProperty()
|
||||
@@ -126,8 +128,7 @@ public class AliasViewByRecordingEditor extends VBox
|
||||
{
|
||||
if(mNoRecordFilteredList == null)
|
||||
{
|
||||
mNoRecordFilteredList = new FilteredList<>(mPlaylistManager.getAliasModel().aliasList(),
|
||||
getNoRecordPredicate());
|
||||
mNoRecordFilteredList = new FilteredList<>(mAliasModel.aliasList(), getNoRecordPredicate());
|
||||
}
|
||||
|
||||
return mNoRecordFilteredList;
|
||||
@@ -137,8 +138,7 @@ public class AliasViewByRecordingEditor extends VBox
|
||||
{
|
||||
if(mRecordFilteredList == null)
|
||||
{
|
||||
mRecordFilteredList = new FilteredList<>(mPlaylistManager.getAliasModel().aliasList(),
|
||||
getRecordPredicate());
|
||||
mRecordFilteredList = new FilteredList<>(mAliasModel.aliasList(), getRecordPredicate());
|
||||
}
|
||||
|
||||
return mRecordFilteredList;
|
||||
|
||||
@@ -32,14 +32,12 @@ import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
@@ -87,14 +85,15 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for playlists
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for preferences
|
||||
*/
|
||||
public AMConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
||||
UserPreferences userPreferences, IFilterProcessor filterProcessor)
|
||||
public AMConfigurationEditor()
|
||||
{
|
||||
super(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
super.postConstruct();
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
||||
|
||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.gui.playlist.channel;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelException;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.control.MaxLengthUnaryOperator;
|
||||
import io.github.dsheirer.gui.playlist.Editor;
|
||||
@@ -31,13 +32,13 @@ import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import javafx.application.Platform;
|
||||
@@ -78,8 +79,13 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelConfigurationEditor.class);
|
||||
|
||||
private PlaylistManager mPlaylistManager;
|
||||
@Resource
|
||||
private AliasModel mAliasModel;
|
||||
@Resource
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
@Resource
|
||||
protected TunerManager mTunerManager;
|
||||
@Resource
|
||||
protected UserPreferences mUserPreferences;
|
||||
protected EditorModificationListener mEditorModificationListener = new EditorModificationListener();
|
||||
private Button mPlayButton;
|
||||
@@ -103,20 +109,20 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for playlists
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for preferences
|
||||
*/
|
||||
public ChannelConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
||||
UserPreferences userPreferences, IFilterProcessor filterProcessor)
|
||||
public ChannelConfigurationEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mTunerManager = tunerManager;
|
||||
mUserPreferences = userPreferences;
|
||||
mFilterProcessor = filterProcessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spring post-construct setup that is common for all sub-class implementations.
|
||||
*
|
||||
* Note: this method is not annotated with @PostConstruct because the subclass method would mask it. So, we
|
||||
* invoke this method from the subclass.
|
||||
*/
|
||||
public void postConstruct()
|
||||
{
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
HBox hbox = new HBox();
|
||||
hbox.setSpacing(10);
|
||||
hbox.setPadding(new Insets(10,10,10,10));
|
||||
@@ -125,16 +131,16 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
HBox.setHgrow(getButtonBox(), Priority.NEVER);
|
||||
hbox.getChildren().addAll(getTextFieldPane(), getButtonBox());
|
||||
VBox.setVgrow(getTitledPanesScrollPane(), Priority.ALWAYS);
|
||||
|
||||
getChildren().addAll(hbox, getTitledPanesScrollPane());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides subclass access to the playlist manager and related components
|
||||
* Sets the filter processor to be controlled by this instance
|
||||
* @param filterProcessor to be controlled.
|
||||
*/
|
||||
protected PlaylistManager getPlaylistManager()
|
||||
public void setFilterProcessor(IFilterProcessor filterProcessor)
|
||||
{
|
||||
return mPlaylistManager;
|
||||
mFilterProcessor = filterProcessor;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -181,7 +187,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
{
|
||||
if(!getAliasListComboBox().getItems().contains(aliasListName))
|
||||
{
|
||||
mPlaylistManager.getAliasModel().addAliasList(aliasListName);
|
||||
mAliasModel.addAliasList(aliasListName);
|
||||
}
|
||||
|
||||
getAliasListComboBox().getSelectionModel().select(aliasListName);
|
||||
@@ -370,7 +376,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
ThreadPool.CACHED.execute(() -> {
|
||||
try
|
||||
{
|
||||
mPlaylistManager.getChannelProcessingManager().start(getItem());
|
||||
mChannelProcessingManager.start(getItem());
|
||||
}
|
||||
catch(ChannelException ce)
|
||||
{
|
||||
@@ -391,7 +397,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
ThreadPool.CACHED.execute(() -> {
|
||||
try
|
||||
{
|
||||
mPlaylistManager.getChannelProcessingManager().stop(getItem());
|
||||
mChannelProcessingManager.stop(getItem());
|
||||
}
|
||||
catch(ChannelException ce)
|
||||
{
|
||||
@@ -623,7 +629,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
{
|
||||
Predicate<String> filterPredicate = s -> !s.contentEquals(AliasModel.NO_ALIAS_LIST);
|
||||
FilteredList<String> filteredChannelList =
|
||||
new FilteredList<>(mPlaylistManager.getAliasModel().aliasListNames(), filterPredicate);
|
||||
new FilteredList<>(mAliasModel.aliasListNames(), filterPredicate);
|
||||
mAliasListComboBox = new ComboBox<>(filteredChannelList);
|
||||
mAliasListComboBox.setPrefWidth(150);
|
||||
mAliasListComboBox.setDisable(true);
|
||||
@@ -659,7 +665,7 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
if(name != null && !name.isEmpty())
|
||||
{
|
||||
name = name.trim();
|
||||
mPlaylistManager.getAliasModel().addAliasList(name);
|
||||
mAliasModel.addAliasList(name);
|
||||
getAliasListComboBox().getSelectionModel().select(name);
|
||||
}
|
||||
});
|
||||
@@ -713,8 +719,8 @@ public abstract class ChannelConfigurationEditor extends Editor<Channel>
|
||||
{
|
||||
try
|
||||
{
|
||||
mPlaylistManager.getChannelProcessingManager().stop(getItem());
|
||||
mPlaylistManager.getChannelProcessingManager().start(getItem());
|
||||
mChannelProcessingManager.stop(getItem());
|
||||
mChannelProcessingManager.start(getItem());
|
||||
}
|
||||
catch(ChannelException se)
|
||||
{
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 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.playlist.channel;
|
||||
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Provides access to channel configuration editors for various decoder types.
|
||||
*/
|
||||
public class ChannelConfigurationEditorFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelConfigurationEditorFactory.class);
|
||||
private static List<DecoderType> mLoggedUnrecognizedTypes = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Constructs an editor for the specified decoder type
|
||||
* @param decoderType to create
|
||||
* @param playlistManager for the editor
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for preferences
|
||||
* @param filterProcessor to be notified to clear and restore any applied filters.
|
||||
* @return constructed editor
|
||||
*/
|
||||
public static ChannelConfigurationEditor getEditor(DecoderType decoderType, PlaylistManager playlistManager,
|
||||
TunerManager tunerManager, UserPreferences userPreferences,
|
||||
IFilterProcessor filterProcessor)
|
||||
{
|
||||
switch(decoderType)
|
||||
{
|
||||
case AM:
|
||||
return new AMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case DMR:
|
||||
return new DMRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case NBFM:
|
||||
return new NBFMConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case LTR_NET:
|
||||
return new LTRNetConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case LTR:
|
||||
return new LTRConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case MPT1327:
|
||||
return new MPT1327ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case PASSPORT:
|
||||
return new PassportConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case P25_PHASE1:
|
||||
return new P25P1ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
case P25_PHASE2:
|
||||
return new P25P2ConfigurationEditor(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
default:
|
||||
if(decoderType != null && !mLoggedUnrecognizedTypes.contains(decoderType))
|
||||
{
|
||||
mLog.warn("Can't create channel configuration editor - unrecognized decoder type: " + decoderType);
|
||||
mLoggedUnrecognizedTypes.add(decoderType);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,11 +21,12 @@ package io.github.dsheirer.gui.playlist.channel;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelModel;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.module.decode.DecoderFactory;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -75,9 +76,13 @@ import org.slf4j.LoggerFactory;
|
||||
public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelEditor.class);
|
||||
private PlaylistManager mPlaylistManager;
|
||||
private TunerManager mTunerManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
@Resource
|
||||
private ChannelModel mChannelModel;
|
||||
@Resource
|
||||
private ChannelProcessingManager mChannelProcessingManager;
|
||||
@Resource
|
||||
private List<ChannelConfigurationEditor> mChannelConfigurationEditors;
|
||||
|
||||
private TableView<Channel> mChannelTableView;
|
||||
private Label mPlaceholderLabel;
|
||||
private MenuButton mNewButton;
|
||||
@@ -91,22 +96,32 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
private ToggleButton mAutoStartToggleButton;
|
||||
private ToggleButton mPlayingToggleButton;
|
||||
private ChannelConfigurationEditor mChannelConfigurationEditor;
|
||||
private UnknownConfigurationEditor mUnknownConfigurationEditor;
|
||||
private ChannelConfigurationEditor mUnknownConfigurationEditor;
|
||||
private Map<DecoderType,ChannelConfigurationEditor> mChannelConfigurationEditorMap = new HashMap();
|
||||
private FilteredList<Channel> mChannelFilteredList;
|
||||
private ChannelListFilter mChannelListFilter = new ChannelListFilter();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager containing playlists and channel configurations
|
||||
*/
|
||||
public ChannelEditor(PlaylistManager playlistManager, TunerManager tunerManager, UserPreferences userPreferences)
|
||||
public ChannelEditor()
|
||||
{
|
||||
mPlaylistManager = playlistManager;
|
||||
mTunerManager = tunerManager;
|
||||
mUserPreferences = userPreferences;
|
||||
mUnknownConfigurationEditor = new UnknownConfigurationEditor(mPlaylistManager, mTunerManager,
|
||||
userPreferences, this);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
//Spring instantiates all editors so place them into a lookup map
|
||||
for(ChannelConfigurationEditor editor: mChannelConfigurationEditors)
|
||||
{
|
||||
mChannelConfigurationEditorMap.put(editor.getDecoderType(), editor);
|
||||
editor.setFilterProcessor(this);
|
||||
|
||||
if(editor instanceof UnknownConfigurationEditor unknown)
|
||||
{
|
||||
mUnknownConfigurationEditor = unknown;
|
||||
}
|
||||
}
|
||||
|
||||
HBox channelsBox = new HBox();
|
||||
channelsBox.setSpacing(10.0);
|
||||
@@ -202,17 +217,12 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
|
||||
if(editor == null)
|
||||
{
|
||||
editor = ChannelConfigurationEditorFactory.getEditor(channelDecoderType, mPlaylistManager,
|
||||
mTunerManager, mUserPreferences, this);
|
||||
mLog.warn("Unable to find channel configuration editor for " + channelDecoderType);
|
||||
|
||||
if(editor != null)
|
||||
for(Map.Entry<DecoderType,ChannelConfigurationEditor> entry: mChannelConfigurationEditorMap.entrySet())
|
||||
{
|
||||
mChannelConfigurationEditorMap.put(channelDecoderType, editor);
|
||||
System.out.println("Type: " + entry.getKey() + " Editor: " + entry.getValue().getClass());
|
||||
}
|
||||
}
|
||||
|
||||
if(editor == null)
|
||||
{
|
||||
editor = mUnknownConfigurationEditor;
|
||||
}
|
||||
|
||||
@@ -228,7 +238,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
{
|
||||
Channel channel = new Channel();
|
||||
channel.setDecodeConfiguration(DecoderFactory.getDecodeConfiguration(decoderType));
|
||||
mPlaylistManager.getChannelModel().addChannel(channel);
|
||||
mChannelModel.addChannel(channel);
|
||||
getChannelTableView().getSelectionModel().select(channel);
|
||||
getChannelTableView().scrollTo(channel);
|
||||
}
|
||||
@@ -498,7 +508,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
mChannelTableView.setPlaceholder(getPlaceholderLabel());
|
||||
|
||||
//Sorting and filtering for the table
|
||||
mChannelFilteredList = new FilteredList<>(mPlaylistManager.getChannelModel().channelList(), mChannelListFilter);
|
||||
mChannelFilteredList = new FilteredList<>(mChannelModel.channelList(), mChannelListFilter);
|
||||
SortedList<Channel> sortedList = new SortedList<>(mChannelFilteredList);
|
||||
sortedList.comparatorProperty().bind(mChannelTableView.comparatorProperty());
|
||||
mChannelTableView.setItems(sortedList);
|
||||
@@ -585,7 +595,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
{
|
||||
try
|
||||
{
|
||||
mPlaylistManager.getChannelProcessingManager().stop(selected);
|
||||
mChannelProcessingManager.stop(selected);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -593,7 +603,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
}
|
||||
}
|
||||
|
||||
mPlaylistManager.getChannelModel().removeChannel(selected);
|
||||
mChannelModel.removeChannel(selected);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -612,7 +622,7 @@ public class ChannelEditor extends SplitPane implements IFilterProcessor
|
||||
mCloneButton.setOnAction(event -> {
|
||||
Channel selected = getChannelTableView().getSelectionModel().getSelectedItem();
|
||||
Channel copy = selected.copyOf();
|
||||
mPlaylistManager.getChannelModel().addChannel(copy);
|
||||
mChannelModel.addChannel(copy);
|
||||
getChannelTableView().getSelectionModel().select(copy);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,12 +32,10 @@ import io.github.dsheirer.module.decode.dmr.DecodeConfigDMR;
|
||||
import io.github.dsheirer.module.decode.dmr.channel.TimeslotFrequency;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -94,14 +92,15 @@ public class DMRConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for playlists
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for preferences
|
||||
*/
|
||||
public DMRConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
||||
UserPreferences userPreferences, IFilterProcessor filterProcessor)
|
||||
public DMRConfigurationEditor()
|
||||
{
|
||||
super(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
super.postConstruct();
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getEventLogPane());
|
||||
|
||||
@@ -31,12 +31,10 @@ import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.ltrstandard.DecodeConfigLTRStandard;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.geometry.HPos;
|
||||
@@ -71,14 +69,15 @@ public class LTRConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for playlists
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for preferences
|
||||
*/
|
||||
public LTRConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
||||
UserPreferences userPreferences, IFilterProcessor filterProcessor)
|
||||
public LTRConfigurationEditor()
|
||||
{
|
||||
super(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
super.postConstruct();
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
||||
|
||||
@@ -31,12 +31,10 @@ import io.github.dsheirer.module.decode.config.DecodeConfiguration;
|
||||
import io.github.dsheirer.module.decode.ltrnet.DecodeConfigLTRNet;
|
||||
import io.github.dsheirer.module.log.EventLogType;
|
||||
import io.github.dsheirer.module.log.config.EventLogConfiguration;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javafx.geometry.HPos;
|
||||
@@ -71,14 +69,15 @@ public class LTRNetConfigurationEditor extends ChannelConfigurationEditor
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for playlists
|
||||
* @param tunerManager for tuners
|
||||
* @param userPreferences for preferences
|
||||
*/
|
||||
public LTRNetConfigurationEditor(PlaylistManager playlistManager, TunerManager tunerManager,
|
||||
UserPreferences userPreferences, IFilterProcessor filterProcessor)
|
||||
public LTRNetConfigurationEditor()
|
||||
{
|
||||
super(playlistManager, tunerManager, userPreferences, filterProcessor);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct()
|
||||
{
|
||||
super.postConstruct();
|
||||
getTitledPanesBox().getChildren().add(getSourcePane());
|
||||
getTitledPanesBox().getChildren().add(getDecoderPane());
|
||||
getTitledPanesBox().getChildren().add(getAuxDecoderPane());
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user