diff --git a/build.gradle b/build.gradle index f0c89909..3e1742ac 100644 --- a/build.gradle +++ b/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 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) diff --git a/src/main/java/io/github/dsheirer/alias/AliasModel.java b/src/main/java/io/github/dsheirer/alias/AliasModel.java index b9d09334..e609d722 100644 --- a/src/main/java/io/github/dsheirer/alias/AliasModel.java +++ b/src/main/java/io/github/dsheirer/alias/AliasModel.java @@ -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); diff --git a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java index beae5d6e..5c8234ba 100644 --- a/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java +++ b/src/main/java/io/github/dsheirer/audio/AbstractAudioModule.java @@ -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()); } } diff --git a/src/main/java/io/github/dsheirer/audio/AudioManager.java b/src/main/java/io/github/dsheirer/audio/AudioManager.java new file mode 100644 index 00000000..9a55c949 --- /dev/null +++ b/src/main/java/io/github/dsheirer/audio/AudioManager.java @@ -0,0 +1,317 @@ +/* + * ***************************************************************************** + * 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 + * **************************************************************************** + */ + +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.preference.UserPreferences; +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.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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Manages handling and distribution of audio segments and calls across the application. + */ +@Component("audioManager") +public class AudioManager implements Listener +{ + private static final Logger LOGGER = LoggerFactory.getLogger(AudioManager.class); + + @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 mCallListeners = new CopyOnWriteArrayList<>(); + private ActiveAudioSegmentProcessor mProcessor = new ActiveAudioSegmentProcessor(); + private ScheduledFuture mProcessorFuture; + + /** + * Constructs an instance + */ + @Autowired + public AudioManager(CallRepository callRepository) + { + mCallRepository = callRepository; + } + + @PostConstruct + public void postConstruct() + { + mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mProcessor, 100, 500, TimeUnit.MILLISECONDS); + + //Register call event listeners + add(mAudioRecordingManager); + add(mAudioPlaybackController); + add(mAudioStreamingManager); + } + + @PreDestroy + public void preDestroy() + { + if(mProcessorFuture != null) + { + mProcessorFuture.cancel(true); + mProcessorFuture = null; + } + LOGGER.info("AudioManager shutting down"); + mProcessor.shutdown(); + } + + /** + * 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(mProcessor.getClass().toString()); + mProcessor.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 mQueuedAudioSegments = new LinkedTransferQueue<>(); + private List mAudioSegmentsToProcess = new ArrayList<>(); + private Map 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 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> 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); + } + } + } +} diff --git a/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java b/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java index 9d3dac9f..c79a5142 100644 --- a/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java +++ b/src/main/java/io/github/dsheirer/audio/DuplicateCallDetector.java @@ -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 { private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class); private CallManagementPreference mCallManagementPreference; private Map 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 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 if(complete) { - audioSegment.decrementConsumerCount(); + audioSegment.removeLease(getClass().toString()); } return complete; @@ -270,7 +287,7 @@ public class DuplicateCallDetector implements Listener if(isDuplicate(current, toCheck)) { toCheck.setDuplicate(true); - toCheck.decrementConsumerCount(); + toCheck.removeLease(getClass().toString()); duplicates.add(toCheck); } } diff --git a/src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java b/src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java index 8a25ffc9..27f6db7f 100644 --- a/src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java +++ b/src/main/java/io/github/dsheirer/audio/IAudioSegmentListener.java @@ -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; /** diff --git a/src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java b/src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java index 41a44ff4..473df899 100644 --- a/src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java +++ b/src/main/java/io/github/dsheirer/audio/IAudioSegmentProvider.java @@ -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; /** diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java index ea6d4d15..39f15269 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/AudioStreamingManager.java @@ -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 +@Component("audioStreamingManager") +public class AudioStreamingManager implements ICallEventListener { private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingManager.class); - private LinkedTransferQueue mNewAudioSegments = new LinkedTransferQueue<>(); - private List mAudioSegments = new ArrayList<>(); - private Listener mAudioRecordingListener; - private BroadcastFormat mBroadcastFormat; + @Resource + private BroadcastModel mBroadcastModel; + @Resource private UserPreferences mUserPreferences; + private LinkedTransferQueue mCompletedCallQueue = new LinkedTransferQueue<>(); + private List 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 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 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 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 processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels()); } } - - audioSegment.decrementConsumerCount(); } + + audioSegment.removeLease(getClass().toString()); + completedCall = mCompletedCallQueue.poll(); } } @@ -235,7 +240,7 @@ public class AudioStreamingManager implements Listener 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 } /** - * 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); } } } diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java index f513938d..9a51ac7e 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastFactory.java @@ -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()); diff --git a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java index 6cd6fdf5..c8754ae4 100644 --- a/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java +++ b/src/main/java/io/github/dsheirer/audio/broadcast/BroadcastModel.java @@ -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 { private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class); @@ -68,28 +71,30 @@ public class BroadcastModel extends AbstractTableModel implements Listener