Compare commits

...

15 Commits

Author SHA1 Message Date
Denny Sheirer
59e74d70b4 #1515 Updates version to 0.6.0 alpha 5 (#1516)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-12 03:14:54 -04:00
Denny Sheirer
8d6b7c3ac8 #1511 Resolves issue with SDRPlay library crashes application when linux service is in stopped state. (#1514)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-11 04:32:58 -04:00
Denny Sheirer
372d2ff870 #1506 Resolves multiple performance issues: tuner freezing after a period of run time, buffer overflows, degraded audio playback. (#1513)
-Reduces message and event histories per channel from 500 to 200.
-Resolves issue of tuner freezing due to cancelled USB transfer buffers not being resubmitted.
-Reworks the audio playback system to remove in-progress call preemption and remove thread locks which can cause glitchy behavior during audio playback.
-Updates audio outputs to also use single thread pool for processing audio segments.
-Updates MBECallSequenceConverter to batch convert MBE to WAV audio.
-Unifies recording filename prefixes across baseband, mbe and binary recordings.
-Resolves issue with registering heartbeat manager to dispatcher.
-Uses single-thread thread pools for Dispatcher to support batch & process to overcome context switching consistency issue in Windows.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-11 04:07:41 -04:00
Denny Sheirer
32a8363b45 Update issue templates
Updated issue templates.
2023-03-25 06:49:58 -04:00
Denny Sheirer
87f1f8986f Update issue templates
Adds issue template to prevent end-users from posting support questions in the issues/bug tracker.
2023-03-25 06:48:08 -04:00
Denny Sheirer
b3c08053fb Update issue templates
Adds feature request template.
2023-03-25 06:42:27 -04:00
Denny Sheirer
a5208dcf1e Update issue templates
Initial bug report template.
2023-03-25 06:40:11 -04:00
Denny Sheirer
2782953518 #1500 Makes Channel Auto-Start timeout a user-preference item to select in range 0-30 seconds. (#1501)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-21 04:32:28 -04:00
Denny Sheirer
366055145e #1498 Updates JMBE audio module to retain class reference instead of reflectively loading the class each time for a new instance. Reduces pressure on both the class loader and the garbage collector. (#1499)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-20 19:56:57 -04:00
Denny Sheirer
39ebdb47b1 #1491 Adds diagnostic logging to dispatcher to support troubleshooting polyphase channel manager's dispatch thread being blocked. (#1492)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-12 09:21:37 -04:00
Denny Sheirer
c788292db8 #1489 SDRPlay RSP tuner editors now lock sample rate combo when sourcing channels. (#1490)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-12 07:21:23 -04:00
Denny Sheirer
f0dc80437c #1487 Updates hot code classes to remove usage of reentrant locks for thread synchronization. (#1488)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 16:04:55 -05:00
Denny Sheirer
2d6c5c2331 #1485 Sets Blackman-Harris 7 As Default FFT Window (#1486)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 15:05:25 -05:00
Denny Sheirer
bc4e6cb5b0 #1473 Adds IF AGC Control for RSP tuners and Power Overload indication. Reverts changes to default MacOS library location in API. (#1484)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 14:58:58 -05:00
Denny Sheirer
d69ad46c10 #1471 Handling of unknown Hytera packet messages. (#1483)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 06:04:06 -05:00
83 changed files with 2121 additions and 1482 deletions

38
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug Report
about: Create a report to help us improve sdrtrunk
title: ''
labels: bug
assignees: ''
---
**sdrtrunk Version**
Indicate which sdrtrunk version you are using. Use 'master-branch' if you are running the latest code.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Application Log**
If applicable, include either the full application log,or a snippet of the error logging.
**Desktop (optional - complete the following information):**
- OS: [e.g. iOS]
- CPU Cores: [e.g. 4 cores]
- RAM:[e.g. 16GB]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,16 @@
---
name: How-To Question or Does The Software Support xxx
about: End user question or support discussion
title: STOP - Don't Use Issue Reporting to Post End-User Questions!!!
labels: ''
assignees: ''
---
STOP !!!!
GitHub issues are for reporting software bugs or requesting enhancements.
If you have a question about how to use the software, or if you want to know if sdrtrunk supports a protocol, tuner, or other aspect, please post your question in one of the Support forums.
Visit: https://github.com/DSheirer/sdrtrunk/wiki

View File

@@ -53,7 +53,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.6.0-alpha4'
version = '0.6.0-alpha5'
//Java 19 is required for this version of the Project Panama preview/incubator feature
java {

View File

@@ -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,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;
@@ -106,7 +105,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
* Gets the current audio segment, or creates a new audio segment as necessary and broadcasts it to any registered
* listener(s).
*/
protected AudioSegment getAudioSegment()
public AudioSegment getAudioSegment()
{
synchronized(this)
{
@@ -135,7 +134,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
}
}
protected void addAudio(float[] audioBuffer)
public void addAudio(float[] audioBuffer)
{
AudioSegment audioSegment = getAudioSegment();

View File

@@ -1,18 +1,21 @@
/*******************************************************************************
* sdr-trunk
* 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 the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
* 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;
@@ -26,15 +29,6 @@ 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 javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -43,6 +37,14 @@ 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;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio segment containing all related metadata and a dynamic collection of audio packets. An audio segment can be
@@ -138,6 +140,15 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mComplete;
}
/**
* Indicates if this audio segment is completed.
* @return true if completed.
*/
public boolean isComplete()
{
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.

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.codec.mbe;
@@ -32,11 +29,6 @@ import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import jmbe.iface.IAudioCodec;
import jmbe.iface.IAudioCodecLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -44,15 +36,20 @@ import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import jmbe.iface.IAudioCodec;
import jmbe.iface.IAudioCodecLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
ISquelchStateListener
{
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
private static List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private IAudioCodec mAudioCodec;
private UserPreferences mUserPreferences;
private final UserPreferences mUserPreferences;
private static Class sLoadedJmbeAudioConverterClass;
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
{
@@ -70,7 +67,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
mAudioCodec = null;
}
protected IAudioCodec getAudioCodec()
public IAudioCodec getAudioCodec()
{
return mAudioCodec;
}
@@ -110,38 +107,67 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
protected abstract String getCodecName();
/**
* Loads audio frame processing chain. Constructs an imbe targetdataline
* to receive the raw imbe frames. Adds an IMBE to 8k PCM format conversion
* stream wrapper. Finally, adds an upsampling (8k to 48k) stream wrapper.
* Loads JMBE audio converter library class and then instantiates new converter instances from the loaded class.
*/
protected void loadConverter()
{
IAudioCodec audioConverter = null;
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
if(path != null)
if(sLoadedJmbeAudioConverterClass == null)
{
try
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
if(path != null)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("Loading JMBE library from [" + path.toString() + "]");
mLog.info("Loading JMBE library from [" + path + "]");
}
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
this.getClass().getClassLoader());
Class classToLoad = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
Object instance = classToLoad.getDeclaredConstructor().newInstance();
if(instance instanceof IAudioCodecLibrary)
try
{
IAudioCodecLibrary library = (IAudioCodecLibrary)instance;
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
this.getClass().getClassLoader());
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
}
catch(IllegalArgumentException iae)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
{
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
}
}
catch(MalformedURLException mue)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(ClassNotFoundException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - class not found");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
}
if(sLoadedJmbeAudioConverterClass != null)
{
try
{
Object instance = sLoadedJmbeAudioConverterClass.getDeclaredConstructor().newInstance();
if(instance instanceof IAudioCodecLibrary library)
{
if((library.getMajorVersion() == 1 && library.getMinorVersion() >= 0 &&
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
{
audioConverter = library.getAudioConverter(getCodecName());
@@ -169,38 +195,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
}
}
}
catch(IllegalArgumentException iae)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
{
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
}
}
catch(NoSuchMethodException nsme)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(MalformedURLException mue)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(ClassNotFoundException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - class not found");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(InvocationTargetException ite)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
@@ -225,6 +219,14 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(NoSuchMethodException nsme)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
else
{
@@ -235,13 +237,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
}
}
if(audioConverter != null)
{
mAudioCodec = audioConverter;
}
else
{
mAudioCodec = null;
}
mAudioCodec = audioConverter;
}
}

View File

@@ -1,27 +1,46 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.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.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
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;
@@ -32,79 +51,130 @@ public class MBECallSequenceConverter
{
private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceConverter.class);
// public static void convert(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);
// }
//
// public static void convert(MBECallSequence callSequence, Path outputPath)
// {
// if(callSequence == null || callSequence.isEncrypted())
// {
// throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
// }
//
// if(callSequence != null && !callSequence.isEncrypted())
// {
// ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
//
// AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
// recorder.start();
//
// long delayMillis = 0;
//
// try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
// {
// thumbDv.start();
// for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
// {
// mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
// thumbDv.decode(voiceFrame.getFrameBytes());
// delayMillis += 30;
// }
//
// if(delayMillis > 0)
// {
// delayMillis += 1000;
// try
// {
// Thread.sleep(delayMillis);
// }
// catch(InterruptedException ie)
// {
//
// }
// }
// }
// catch(IOException ioe)
// {
// mLog.error("Error", ioe);
// }
//
// recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
// }
// }
//
// public static void main(String[] args)
// {
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_3_TS0_65084_6591007.mbe";
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe";
//
// Path input = Paths.get(mbe);
// Path output = Paths.get(mbe.replace(".mbe", ".tmp"));
//
// mLog.info("Converting: " + mbe);
//
// try
// {
// MBECallSequenceConverter.convert(input, output);
// }
// catch(IOException ioe)
// {
// mLog.error("Error", ioe);
// }
// }
/**
* Converts the input MBE file to PCM audio and writes to the output wave file.
* @param input path to the MBE file.
* @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
{
InputStream inputStream = Files.newInputStream(input);
ObjectMapper mapper = new ObjectMapper();
MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
convert(sequence, output);
}
public static void convert(MBECallSequence callSequence, Path outputPath)
{
if(callSequence == null || callSequence.isEncrypted())
{
throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
}
if(callSequence != null)
{
if(callSequence.getProtocol().equals(P25P1CallSequenceRecorder.PROTOCOL))
{
P25P1AudioModule audioModule = new P25P1AudioModule(new UserPreferences(), new AliasList("mbe generator"));
audioModule.setRecordAudio(true);
audioModule.start();
if(callSequence.getFromIdentifier() != null)
{
int from = 0;
try
{
from = Integer.parseInt(callSequence.getFromIdentifier());
audioModule.getIdentifierUpdateListener().receive(new IdentifierUpdateNotification(APCO25RadioIdentifier.createFrom(from),
IdentifierUpdateNotification.Operation.ADD, 0));
}
catch(Exception e)
{
mLog.error("Error parsing from identifier from value [" + callSequence.getFromIdentifier());
}
}
if(callSequence.getToIdentifier() != null)
{
int to = 0;
try
{
to = Integer.parseInt(callSequence.getToIdentifier());
audioModule.getIdentifierUpdateListener().receive(new IdentifierUpdateNotification(APCO25Talkgroup.create(to),
IdentifierUpdateNotification.Operation.ADD, 0));
}
catch(Exception e)
{
mLog.error("Error parsing from identifier from value [" + callSequence.getFromIdentifier());
}
}
IAudioCodec codec = audioModule.getAudioCodec();
for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
{
byte[] frameBytes = voiceFrame.getFrameBytes();
float[] audio = codec.getAudio(frameBytes);
audioModule.addAudio(audio);
}
AudioSegment audioSegment = audioModule.getAudioSegment();
try
{
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
}
catch(IOException ioe)
{
mLog.error("Error writing audio segment, ioe");
}
audioModule.stop();
}
}
}
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);
}
}
}
}

View File

@@ -1,30 +1,27 @@
/*
* *****************************************************************************
* 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.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.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;
@@ -33,11 +30,13 @@ 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.util.ThreadPool;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import java.nio.ByteBuffer;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,10 +50,6 @@ import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import java.nio.ByteBuffer;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
@@ -67,7 +62,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private int mBufferStopThreshold;
private Listener<IdentifierCollection> mIdentifierCollectionListener;
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
private ScheduledFuture<?> mProcessorFuture;
private SourceDataLine mOutput;
private Mixer mMixer;
private MixerChannel mMixerChannel;
@@ -76,17 +70,17 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private boolean mCanProcessAudio = false;
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private AudioSegment mCurrentAudioSegment;
private AudioSegment mNextAudioSegment;
private ReentrantLock mLock = new ReentrantLock();
private int mCurrentBufferIndex = 0;
private UserPreferences mUserPreferences;
private BooleanProperty mEmptyProperty = new SimpleBooleanProperty(true);
private IntegerProperty mAudioPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
private ByteBuffer mAudioSegmentStartTone;
private ByteBuffer mAudioSegmentPreemptTone;
private ByteBuffer mAudioSegmentDropTone;
private boolean mRunning = false;
private ScheduledExecutorService mScheduledExecutorService;
private ScheduledFuture<?> mProcessorFuture;
private long mOutputLastTimestamp = 0;
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
/**
* Single audio channel playback with automatic starting and stopping of the
@@ -107,6 +101,8 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
mMixer = mixer;
mMixerChannel = mixerChannel;
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mixerChannel.name()));
mUserPreferences = userPreferences;
try
@@ -150,7 +146,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
//Run the queue processor task every 100 milliseconds or 10 times a second
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
}
@@ -172,26 +168,17 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
/**
* Boolean property that indicates if this audio output has an audio segment queued or in process of playback.
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
* @return true if empty
*/
public BooleanProperty emptyProperty()
public boolean isEmpty()
{
return mEmptyProperty;
return mAudioSegmentQueue.isEmpty();
}
/**
* Audio playback priority of the current audio segment or default audio priority if nothing is currently loaded.
*/
public IntegerProperty audioPriorityProperty()
{
return mAudioPriority;
}
/**
* Schedules the audio segment for playback.
*
* Note: if a segment is currently playing and another audio segment is already queued for playback, invoking
* this method will overwrite the queued segment with the argument.
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
* calling entity.
*
* @param audioSegment to schedule for playback.
*/
@@ -199,23 +186,8 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
if(audioSegment != null)
{
mLock.lock();
if(mNextAudioSegment != null)
{
mNextAudioSegment.decrementConsumerCount();
mNextAudioSegment = null;
}
try
{
mNextAudioSegment = audioSegment;
mEmptyProperty.set(false);
}
finally
{
mLock.unlock();
}
//Audio segment use count has already been incremented by the external caller.
mAudioSegmentQueue.add(audioSegment);
}
}
@@ -262,7 +234,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private void updateToneInsertionAudioClips()
{
mAudioSegmentStartTone = null;
mAudioSegmentPreemptTone = null;
mAudioSegmentDropTone = null;
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
@@ -272,13 +243,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
mAudioSegmentStartTone = convert(start);
}
float[] preempt = mUserPreferences.getPlaybackPreference().getPreemptTone();
if(preempt != null)
{
mAudioSegmentPreemptTone = convert(preempt);
}
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
if(drop != null)
@@ -287,48 +251,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
}
/**
* Disposes of a processed audio segment and notifies the originator that processing of the segment is complete
* by decrementing the consumer count.
* @param audioSegment to dispose
*/
private void dispose(AudioSegment audioSegment)
{
mAudioPriority.unbind();
if(audioSegment != null)
{
audioSegment.decrementConsumerCount();
audioSegment.removeIdentifierUpdateNotificationListener(this);
}
}
/**
* Generates a tone indicating that a new audio segment is starting
*/
private ByteBuffer getAudioSegmentStartTone()
{
return mAudioSegmentStartTone;
}
/**
* Generates a tone indicating that the current audio segment playback has been preempted for a higher priority
* audio segment that is now starting.
*/
private ByteBuffer getAudioSegmentPreemptionTone()
{
return mAudioSegmentPreemptTone;
}
/**
* Generates a tone indicating that the current audio segment playback has been dropped because the audio segment
* has been flagged as Do Not Monitor after playback has started.
*/
private ByteBuffer getAudioSegmentDropTone()
{
return mAudioSegmentDropTone;
}
/**
* Writes the audio buffer data to the source data line. If the data line is not currently playing, write as
* much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual
@@ -355,6 +277,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
//Top off the buffer and check if we can start it
wrote += mOutput.write(buffer.array(), 0, toWrite);
mOutputLastTimestamp = System.currentTimeMillis();
checkStart();
}
@@ -363,147 +286,135 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
//This will block until the buffer is fully written to the data line
mOutput.write(buffer.array(), wrote, buffer.array().length - wrote);
mOutputLastTimestamp = System.currentTimeMillis();
}
}
}
/**
* Dispose of the currently assigned audio segment.
*/
private void disposeCurrentAudioSegment()
{
if(mCurrentAudioSegment != null)
{
mCurrentAudioSegment.decrementConsumerCount();
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
mCurrentAudioSegment = null;
}
}
/**
* Loads the next audio segment from the queue.
*/
private void loadNextAudioSegment()
{
AudioSegment audioSegment = mAudioSegmentQueue.poll();
boolean verificationInProgress = (audioSegment != null);
while(verificationInProgress)
{
if(audioSegment != null)
{
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
if(isThrowaway(audioSegment))
{
audioSegment.decrementConsumerCount();
audioSegment = mAudioSegmentQueue.poll();
}
else
{
verificationInProgress = false;
}
}
else
{
verificationInProgress = false;
}
}
mCurrentAudioSegment = audioSegment;
if(audioSegment != null)
{
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
//If we played any amount of the previous audio segment then play the audio segment start. We check this
//to avoid repeatedly playing an audio segment start tone on the rare occasion that we have empty segments.
if(mCurrentBufferIndex > 0)
{
playAudio(mAudioSegmentStartTone);
}
}
mCurrentBufferIndex = 0;
}
/**
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
* @param audioSegment to evaluate
* @return true if the audio should be thrown away.
*/
private boolean isThrowaway(AudioSegment audioSegment)
{
return audioSegment != null &&
(audioSegment.isDoNotMonitor() ||
(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference()
.isDuplicatePlaybackSuppressionEnabled()));
}
/**
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
* by a threaded processor repeatedly to playback the current audio segment and check for and start a newly
* assigned audio segment. It also handles starting and stopping the playback source data line to avoid audio
* by a threaded processor repeatedly to playback the current audio segment and check for and start newly
* added audio segments. It also handles starting and stopping the playback source data line to avoid audio
* discontinuities due to buffer underruns.
*/
private void processAudio()
{
if(mNextAudioSegment != null)
if(mCurrentAudioSegment == null)
{
mLock.lock();
try
{
//Remove an assigned segment that's subsequently flagged as duplicate or do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor() || (mNextAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
mNextAudioSegment = null;
if(mCurrentAudioSegment == null)
{
mEmptyProperty.set(true);
}
}
//For linked audio segments, allow the linked segment to complete first before assigning the next
else if(mNextAudioSegment.isLinked())
{
if(mCurrentAudioSegment == null)
{
mCurrentAudioSegment = mNextAudioSegment;
mNextAudioSegment = null;
mCurrentBufferIndex = 0;
if(mCurrentAudioSegment != null)
{
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
else
{
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
}
}
}
else
{
//Insert audio segment start or audio priority preemption bonk tone
if(mCurrentAudioSegment == null)
{
playAudio(getAudioSegmentStartTone());
}
else if(mCurrentBufferIndex > 0 &&
(!mCurrentAudioSegment.completeProperty().get() ||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount()))
{
playAudio(getAudioSegmentPreemptionTone());
}
else
{
playAudio(getAudioSegmentStartTone());
}
//Close current audio segment
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = mNextAudioSegment;
mNextAudioSegment = null;
mCurrentBufferIndex = 0;
if(mCurrentAudioSegment != null)
{
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
else
{
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
}
}
}
finally
{
mLock.unlock();
}
loadNextAudioSegment();
}
if(mCurrentAudioSegment != null)
//Reevaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
if(isThrowaway(mCurrentAudioSegment))
{
//Check for completed audio segment or a segment flagged as duplicate or Do Not Monitor
if(mCurrentAudioSegment.isDoNotMonitor() ||
(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()) ||
(mCurrentAudioSegment.completeProperty().get() && mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
if(mCurrentBufferIndex > 0)
{
if(mCurrentAudioSegment.isDoNotMonitor())
{
playAudio(getAudioSegmentDropTone());
}
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = null;
mLock.lock();
try
{
if(mNextAudioSegment == null)
{
mEmptyProperty.set(true);
}
}
finally
{
mLock.unlock();
}
return;
playAudio(mAudioSegmentDropTone);
}
//Process any new buffers that have been added to the audio segment. If a next audio segment gets assigned
//while processing, exit the loop so that we can evaluate the next for higher priority preempt. If the next
//segment is a linked segment, ignore it so that we can close out the current segment.
while(mCurrentAudioSegment != null && (mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
!mCurrentAudioSegment.isDoNotMonitor() && !(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
disposeCurrentAudioSegment();
loadNextAudioSegment();
}
while(mCurrentAudioSegment != null && mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
{
try
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
if(audioBuffer != null)
{
ByteBuffer audio = convert(audioBuffer);
//This call blocks until all audio bytes are dumped into the data line.
playAudio(audio);
}
}
catch(Exception e)
{
mLog.error("Error while processing audio for [" + mMixerChannel.name() + "]", e);
}
}
//Check for completed and fully-played audio segment -- load next audio segment
if(mCurrentAudioSegment != null &&
mCurrentAudioSegment.isComplete() &&
(mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
{
disposeCurrentAudioSegment();
}
checkStop();
@@ -523,25 +434,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
mProcessorFuture = null;
mLock.lock();
try
{
if(mNextAudioSegment != null)
{
mNextAudioSegment.decrementConsumerCount();
mNextAudioSegment = null;
}
}
finally
{
mLock.unlock();
}
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = null;
disposeCurrentAudioSegment();
mAudioEventBroadcaster.clear();
mIdentifierCollectionListener = null;
@@ -650,14 +543,28 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(mRunning)
{
//If the output buffer falls below the threshold then drain the output and stop playback
if(mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
if(mOutput.isRunning())
{
mOutput.drain();
mOutput.stop();
mRunning = false;
if(mOutput.available() >= mBufferStopThreshold)
{
mOutput.drain();
mOutput.stop();
mRunning = false;
}
//Detect when we have a partially filled output that will neither start nor stop and the last write
//timestamp was more than the staleness threshold (500 ms) -- stop the output and discard stale contents
else if(mOutput.available() < mBufferStopThreshold &&
mOutput.available() > mBufferStartThreshold &&
(System.currentTimeMillis() - mOutputLastTimestamp >= STALE_PLAYBACK_THRESHOLD_MS))
{
mOutput.stop();
//Discard the stale buffer contents.
mOutput.flush();
mRunning = false;
}
}
//If output playback stopped on its own because the buffer emptied, then cleanup
else if(!mOutput.isRunning())
else
{
mRunning = false;
}
@@ -735,16 +642,33 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public class AudioSegmentProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
try
if(mProcessing.compareAndSet(false, true))
{
processAudio();
}
catch(Throwable t)
{
mLog.error("Error while processing audio buffers", t);
try
{
//TODO: Debug hooks - remove after testing
if(mMixerChannel.name().equals("LEFT"))
{
int a = 0;
}
else if(mMixerChannel.name().equals("RIGHT"))
{
int a = 0;
}
processAudio();
}
catch(Throwable t)
{
mLog.error("Error while processing audio buffers", t);
}
mProcessing.set(false);
}
}
}

View File

@@ -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,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,6 +23,7 @@ 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.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
@@ -31,17 +31,18 @@ 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 io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages scheduling and playback of audio segments to the local users audio system.
@@ -50,8 +51,6 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
@@ -64,6 +63,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
private ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
/**
* Constructs an instance.
@@ -202,29 +203,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
//Sort audio segments by playback priority and assign to empty audio outputs
if(!mAudioSegments.isEmpty())
{
//TODO: change this to take audio segment start time into account also
mAudioSegments.sort(Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
{
if(audioOutput.emptyProperty().get())
{
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
{
return;
}
}
}
}
//Preempt ongoing audio segment playback when higher priority audio segments are available
if(!mAudioSegments.isEmpty())
{
for(AudioOutput audioOutput: mAudioOutputs)
{
if(mAudioSegments.get(0).monitorPriorityProperty().get() < audioOutput.audioPriorityProperty().get())
if(audioOutput.isEmpty())
{
audioOutput.play(mAudioSegments.remove(0));
@@ -284,7 +269,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
catch(AudioException ae)
{
mLog.error("Error changing audio output to [" + configuration.toString() + "]", ae);
mLog.error("Error changing audio output to [" + configuration + "]", ae);
}
}
}
@@ -332,7 +317,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
}
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
mMixerChannelConfiguration = entry;
@@ -343,7 +328,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
* Current audio playback mixer channel configuration setting.
*/
@Override
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
public MixerChannelConfiguration getMixerChannelConfiguration()
{
return mMixerChannelConfiguration;
}
@@ -384,16 +369,23 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
*/
public class AudioSegmentProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
try
if(mProcessing.compareAndSet(false, true))
{
processAudioSegments();
}
catch(Throwable t)
{
mLog.error("Encountered error while processing audio segments", t);
try
{
processAudioSegments();
}
catch(Throwable t)
{
mLog.error("Encountered error while processing audio segments", t);
}
mProcessing.set(false);
}
}
}

View File

@@ -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,22 +14,13 @@
*
* 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;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
@@ -41,12 +31,21 @@ import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
public class ChannelAutoStartFrame extends JFrame
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelAutoStartFrame.class);
private static final int COUNTDOWN_SECONDS = 10;
private Listener<ChannelEvent> mChannelEventListener;
private List<Channel> mChannels;
@@ -55,7 +54,7 @@ public class ChannelAutoStartFrame extends JFrame
private JButton mCancelButton;
private JTable mChannelTable;
private AtomicBoolean mChannelsStarted = new AtomicBoolean();
private int mAutoStartTimeoutSeconds;
private ScheduledFuture<?> mTimerFuture;
/**
@@ -66,10 +65,12 @@ public class ChannelAutoStartFrame extends JFrame
* @param listener to receive channel start/enable request(s)
* @param channels to auto-start
*/
public ChannelAutoStartFrame(Listener<ChannelEvent> listener, List<Channel> channels)
public ChannelAutoStartFrame(Listener<ChannelEvent> listener, List<Channel> channels, UserPreferences userPreferences)
{
mChannelEventListener = listener;
mChannels = channels;
mAutoStartTimeoutSeconds = userPreferences.getApplicationPreference().getChannelAutoStartTimeout();
init();
EventQueue.invokeLater(() -> {
@@ -100,7 +101,7 @@ public class ChannelAutoStartFrame extends JFrame
"[][][grow,fill][]"));
panel.add(new JLabel("The following channels will be automatically"), "span");
mCountdownLabel = new JLabel(getCountdownText(COUNTDOWN_SECONDS));
mCountdownLabel = new JLabel(getCountdownText(mAutoStartTimeoutSeconds));
panel.add(mCountdownLabel, "span");
panel.add(new JScrollPane(getChannelTable()), "span");
@@ -220,7 +221,7 @@ public class ChannelAutoStartFrame extends JFrame
*/
public class CountdownTimer implements Runnable
{
private int mCount = COUNTDOWN_SECONDS;
private int mCount = mAutoStartTimeoutSeconds;
@Override
public void run()

View File

@@ -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
@@ -623,30 +623,27 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
Platform.runLater(() -> channel.setProcessing(false));
}
//Since the processing chain's source will block on stop(), throw it to the thread pool to run
ThreadPool.CACHED.submit(() -> {
try
{
processingChain.stop();
processingChain.removeEventLoggingModules();
processingChain.removeRecordingModules();
try
{
processingChain.stop();
processingChain.removeEventLoggingModules();
processingChain.removeRecordingModules();
//Deregister channel from receive frequency correction events to show in the spectral display (hack!)
processingChain.removeFrequencyChangeListener(channel);
channel.resetFrequencyCorrection();
//Deregister channel from receive frequency correction events to show in the spectral display (hack!)
processingChain.removeFrequencyChangeListener(channel);
channel.resetFrequencyCorrection();
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
mChannelEventBroadcaster.removeListener(processingChain);
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
mChannelEventBroadcaster.removeListener(processingChain);
//Unregister for event bus requests and notifications
processingChain.getEventBus().unregister(ChannelProcessingManager.this);
processingChain.dispose();
}
catch(Exception e)
{
mLog.error("Error during shutdown of processing chain for channel [" + channel.getName() + "}", e);
}
});
//Unregister for event bus requests and notifications
processingChain.getEventBus().unregister(ChannelProcessingManager.this);
processingChain.dispose();
}
catch(Exception e)
{
mLog.error("Error during shutdown of processing chain for channel [" + channel.getName() + "}", e);
}
}
}

View File

@@ -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
@@ -122,6 +122,11 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
mChannels.add(polyphaseChannelSource);
mSourceChangeBroadcaster.addListener(polyphaseChannelSource.getSourceEventListener());
}
else
{
mLog.error("Error adding polyphase channel source - " + (polyphaseChannelSource == null ? "source is null" :
"channel source is already added to this channelizer"));
}
}
/**
@@ -136,6 +141,11 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
mChannels.remove(polyphaseChannelSource);
mSourceChangeBroadcaster.removeListener(polyphaseChannelSource.getSourceEventListener());
}
else
{
mLog.error("Error removing polyphase channel source - " + (polyphaseChannelSource == null ? "source is null" :
"channel source was not previously added to this channelizer"));
}
}
/**

View File

@@ -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
@@ -24,7 +24,6 @@ import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.util.Dispatcher;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
@@ -69,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
//Sized at 152 buffers a second where max = 5 seconds
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(5 * 152);
//Sized to process 1/20th batch of 152 buffers a second, at 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(152 / 20, 25);
private FloatFFT_1D mFFT;
private float[] mInlineSamples;
private float[] mInlineFilter;
@@ -407,25 +406,32 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
{
public IFFTProcessorDispatcher(int maximumSize)
public IFFTProcessorDispatcher(int batchSize, long interval)
{
super(maximumSize, "sdrtrunk polyphase ifft processor", Collections.emptyList());
super("sdrtrunk polyphase ifft processor", batchSize, interval);
//We create a listener interface to receive the batched channel results arrays from the scheduled thread pool
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each
//channel results array contained in each results buffer and then dispatch the buffer
//so that it can be distributed to each channel listener.
setListener(list -> {
List<float[]> processedChannelResults = new ArrayList<>();
for(float[] channelResults: list)
try
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
processedChannelResults.add(channelResults);
}
List<float[]> processedChannelResults = new ArrayList<>();
dispatch(processedChannelResults);
for(float[] channelResults: list)
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
processedChannelResults.add(channelResults);
}
dispatch(processedChannelResults);
}
catch(Throwable t)
{
mLog.error("Error during IFFT and dispatch of processed channel results", t);
}
});
}
}

View File

@@ -20,7 +20,6 @@ package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.INativeBufferProvider;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.eventbus.MyEventBus;
@@ -28,7 +27,6 @@ import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.source.ISourceEventProcessor;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerController;
@@ -44,7 +42,6 @@ import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -111,8 +108,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
mBufferDispatcher = new Dispatcher(500, "sdrtrunk polyphase buffer processor",
new NativeBufferPoisonPill());
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 50, 10);
mBufferDispatcher.setListener(mNativeBufferReceiver);
}
@@ -178,14 +174,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
}
/**
* Current channel sample rate which is (2 * channel bandwidth).
*/
public double getChannelSampleRate()
{
return mChannelCalculator.getChannelSampleRate();
}
/**
* Current channel bandwidth/spacing.
*/
@@ -440,9 +428,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
break;
case REQUEST_STOP_SAMPLE_STREAM:
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource channelSource)
{
stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
stopChannelSource(channelSource);
channelSource.dispose();
}
else
{
@@ -450,14 +439,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
}
break;
case REQUEST_SOURCE_DISPOSE:
Source source = sourceEvent.getSource();
if(source instanceof PolyphaseChannelSource)
{
source.dispose();
}
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast so that the tuner source can process this event
mSourceEventBroadcaster.broadcast(sourceEvent);
@@ -481,7 +462,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
public class NativeBufferReceiver implements Listener<INativeBuffer>
{
private boolean mOutputProcessorUpdateRequired = false;
private ReentrantLock mUpdateFlagLock = new ReentrantLock();
/**
* Processes tuner center frequency change source events to flag when output processors need updating.
@@ -493,18 +473,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
if(mChannelCalculator.getCenterFrequency() != frequency)
{
mUpdateFlagLock.lock();
try
{
//Update the channel calculator frequency so that it's ready when the output processor update occurs
mChannelCalculator.setCenterFrequency(frequency);
mOutputProcessorUpdateRequired = true;
}
finally
{
mUpdateFlagLock.unlock();
}
//Update the channel calculator frequency so that it's ready when the output processor update occurs
mChannelCalculator.setCenterFrequency(frequency);
mOutputProcessorUpdateRequired = true;
}
}
@@ -516,19 +487,17 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
@Override
public void receive(INativeBuffer nativeBuffer)
{
mUpdateFlagLock.lock();
try
if(mOutputProcessorUpdateRequired)
{
if(mOutputProcessorUpdateRequired)
try
{
updateOutputProcessors();
mOutputProcessorUpdateRequired = false;
}
}
finally
{
mUpdateFlagLock.unlock();
catch(Exception e)
{
mLog.error("Error updating polyphase channel output processors");
}
mOutputProcessorUpdateRequired = false;
}
if(mPolyphaseChannelizer != null)

View File

@@ -25,15 +25,12 @@ import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.StreamProcessorWithHeartbeat;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Polyphase Channelizer's Tuner Channel Source implementation. Wraps a ChannelOutputProcessor instance and
@@ -44,15 +41,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
private Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
private Listener<ComplexSamples> mSamplesListener;
private double mChannelSampleRate;
private long mIndexCenterFrequency;
private ReentrantLock mOutputProcessorLock = new ReentrantLock();
private List<Integer> mOutputProcessorIndexes = new ArrayList<>();
private double mTunerSampleRate;
private double mTunerCenterFrequency;
private PendingOutputProcessorUpdate mPendingOutputProcessorUpdate;
/**
* Constructs an instance
@@ -68,8 +63,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
super(producerSourceEventListener, tunerChannel);
mChannelSampleRate = channelCalculator.getChannelSampleRate();
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
updateOutputProcessor(channelCalculator, filterManager);
doUpdateOutputProcessor(channelCalculator, filterManager);
}
/**
@@ -112,8 +106,6 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
super.start();
mStreamHeartbeatProcessor.start();
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.start();
@@ -129,8 +121,6 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
mPolyphaseChannelOutputProcessor.stop();
}
mStreamHeartbeatProcessor.stop();
}
/**
@@ -139,113 +129,129 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
@Override
public void setListener(final Listener<ComplexSamples> listener)
{
mStreamHeartbeatProcessor.setListener(listener);
mSamplesListener = listener;
mPolyphaseChannelOutputProcessor.setListener(this);
}
@Override
public void receive(ComplexSamples complexSamples)
{
mStreamHeartbeatProcessor.receive(complexSamples);
if(mSamplesListener != null)
{
try
{
mSamplesListener.receive(complexSamples);
}
catch(Throwable t)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener.getClass() + "]", t);
}
}
}
/**
* Updates the output processor whenever the source tuner's center frequency changes.
* Queues a request to update the output processor whenever the source tuner's center frequency changes.
* @param channelCalculator providing access to updated tuner center frequency, sample rate, etc.
* @param filterManager for designing and caching synthesis filters
*/
public void updateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager) throws IllegalArgumentException
public void updateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager)
throws IllegalArgumentException
{
mPendingOutputProcessorUpdate = new PendingOutputProcessorUpdate(channelCalculator, filterManager);
}
/**
* Performs the output processor update. This method must be invoked on the channel results calling thread to
* ensure we don't have thread contention on the output processor.
* @param channelCalculator for updates
* @param filterManager for updates
* @throws IllegalArgumentException if there's an issue.
*/
public void doUpdateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager)
throws IllegalArgumentException
{
String errorMessage = null;
mOutputProcessorLock.lock();
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = channelCalculator.getChannelIndexes(getTunerChannel());
try
mOutputProcessorIndexes.clear();
mOutputProcessorIndexes.addAll(indexes);
mTunerCenterFrequency = channelCalculator.getCenterFrequency();
mTunerSampleRate = channelCalculator.getSampleRate();
//The provided channels are necessarily aligned to the center frequency that this source is providing and an
//oscillator will mix the provided channels to bring the desired center frequency to baseband.
setFrequency(channelCalculator.getCenterFrequencyForIndexes(indexes));
if(mPolyphaseChannelOutputProcessor != null && mPolyphaseChannelOutputProcessor.getInputChannelCount() == indexes.size())
{
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = channelCalculator.getChannelIndexes(getTunerChannel());
mPolyphaseChannelOutputProcessor.setPolyphaseChannelIndices(indexes);
mOutputProcessorIndexes.clear();
mOutputProcessorIndexes.addAll(indexes);
mTunerCenterFrequency = channelCalculator.getCenterFrequency();
mTunerSampleRate = channelCalculator.getSampleRate();
//The provided channels are necessarily aligned to the center frequency that this source is providing and an
//oscillator will mix the provided channels to bring the desired center frequency to baseband.
setFrequency(channelCalculator.getCenterFrequencyForIndexes(indexes));
if(mPolyphaseChannelOutputProcessor != null && mPolyphaseChannelOutputProcessor.getInputChannelCount() == indexes.size())
if(indexes.size() > 1)
{
mPolyphaseChannelOutputProcessor.setPolyphaseChannelIndices(indexes);
if(indexes.size() > 1)
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), indexes.size());
mPolyphaseChannelOutputProcessor.setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
errorMessage ="Cannot update output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices - channel sample rate [" + channelCalculator.getChannelSampleRate() +
"] channel bandwidth [" + channelCalculator.getChannelBandwidth() + "]";
}
}
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
else //Create a new output processor.
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.setListener(null);
mPolyphaseChannelOutputProcessor.stop();
}
mPolyphaseChannelOutputProcessor = null;
switch(indexes.size())
{
case 1:
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, channelCalculator.getChannelCount(), getHeartbeatManager());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
break;
case 2:
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), indexes.size());
mPolyphaseChannelOutputProcessor.setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
errorMessage ="Cannot update output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
}
}
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
else //Create a new output processor.
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.setListener(null);
mPolyphaseChannelOutputProcessor.stop();
}
mPolyphaseChannelOutputProcessor = null;
switch(indexes.size())
{
case 1:
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, channelCalculator.getChannelCount());
channelCalculator.getChannelBandwidth(), 2);
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, filter, channelCalculator.getChannelCount(), getHeartbeatManager());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
break;
case 2:
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), 2);
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, filter, channelCalculator.getChannelCount());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
}
catch(FilterDesignException fde)
{
errorMessage = "Cannot create new output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
mLog.error("Error creating a synthesis filter for a new channel output processor");
}
break;
default:
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(channelCalculator.toString());
errorMessage = "Unable to create new channel output processor - unexpected channel index size: " +
indexes.size();
}
}
catch(FilterDesignException fde)
{
errorMessage = "Cannot create new output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
mLog.error("Error creating a synthesis filter for a new channel output processor");
}
break;
default:
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(channelCalculator.toString());
errorMessage = "Unable to create new channel output processor - unexpected channel index size: " +
indexes.size();
}
}
finally
{
mOutputProcessorLock.unlock();
}
//Unlikely, but if we had an error designing a synthesis filter, throw an exception
if(errorMessage != null)
@@ -264,7 +270,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
*/
public void receiveChannelResults(List<float[]> channelResultsList, long currentSamplesTimestamp)
{
mOutputProcessorLock.lock();
if(mPendingOutputProcessorUpdate != null)
{
ChannelCalculator channelCalculator = mPendingOutputProcessorUpdate.getChannelCalculator();
SynthesisFilterManager filterManager = mPendingOutputProcessorUpdate.getSynthesisFilterManager();
mPendingOutputProcessorUpdate = null;
doUpdateOutputProcessor(channelCalculator, filterManager);
}
try
{
@@ -273,9 +285,9 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList, currentSamplesTimestamp);
}
}
finally
catch(Exception e)
{
mOutputProcessorLock.unlock();
mLog.error("Error processing channel results", e);
}
}
@@ -344,4 +356,40 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
getTunerChannel().getFrequency();
}
/**
* Support classes needed to update the output processor.
*/
public class PendingOutputProcessorUpdate
{
private final ChannelCalculator mChannelCalculator;
private final SynthesisFilterManager mSynthesisFilterManager;
/**
* Constructs an instance
* @param channelCalculator for support
* @param synthesisFilterManager for support
*/
public PendingOutputProcessorUpdate(ChannelCalculator channelCalculator, SynthesisFilterManager synthesisFilterManager)
{
mChannelCalculator = channelCalculator;
mSynthesisFilterManager = synthesisFilterManager;
}
/**
* Channel calculator
*/
public ChannelCalculator getChannelCalculator()
{
return mChannelCalculator;
}
/**
* Channel calculator
*/
public SynthesisFilterManager getSynthesisFilterManager()
{
return mSynthesisFilterManager;
}
}
}

View File

@@ -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,8 +20,8 @@ package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import io.github.dsheirer.util.Dispatcher;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,6 +31,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class);
private Dispatcher<List<float[]>> mChannelResultsDispatcher;
private HeartbeatManager mHeartbeatManager;
protected Listener<ComplexSamples> mComplexSamplesListener;
private int mInputChannelCount;
private long mCurrentSampleTimestamp = System.currentTimeMillis();
@@ -42,12 +43,25 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
* @param inputChannelCount is the number of input channels for this output processor
* @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
* channel sample rate for frequency translation/correction.
* @param heartbeatManager to receive pings on the dispatcher thread
*/
public ChannelOutputProcessor(int inputChannelCount, double sampleRate)
public ChannelOutputProcessor(int inputChannelCount, double sampleRate, HeartbeatManager heartbeatManager)
{
mInputChannelCount = inputChannelCount;
mChannelResultsDispatcher = new Dispatcher<>((int)sampleRate, "sdrtrunk polyphase channel", Collections.emptyList());
mChannelResultsDispatcher.setListener(floats -> process(floats));
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
mHeartbeatManager = heartbeatManager;
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel", (int)(sampleRate / 10),
50, mHeartbeatManager);
mChannelResultsDispatcher.setListener(floats -> {
try
{
process(floats);
}
catch(Throwable t)
{
mLog.error("Error processing channel results", t);
}
});
}
/**

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,10 +37,12 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
* @param sampleRate of the output sample stream.
* @param channelIndexes containing a single channel index.
* @param gain value to apply. This is typically the same as the channelizer's channel count.
* @param heartbeatManager to receive heartbeats on the dispatch thread
*/
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float gain)
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float gain,
HeartbeatManager heartbeatManager)
{
super(1, sampleRate);
super(1, sampleRate, heartbeatManager);
setPolyphaseChannelIndices(channelIndexes);
mMixerAssembler = new OneChannelMixerAssembler(gain);
mMixerAssembler.getMixer().setSampleRate(sampleRate);

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,12 +38,14 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
* @param sampleRate of the output sample stream.
* @param channelIndexes containing two channel indices.
* @param gain to apply to output. Typically this is equal to the channelizer's channel count.
* @param heartbeatManager to be pinged on the dispatcher thread
*/
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter, float gain)
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter, float gain,
HeartbeatManager heartbeatManager)
{
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
//after synthesizing both input channels
super(2, sampleRate);
super(2, sampleRate, heartbeatManager);
setPolyphaseChannelIndices(channelIndexes);
mMixerAssembler = new TwoChannelMixerAssembler(gain);
mMixerAssembler.getMixer().setSampleRate(sampleRate);

View File

@@ -313,7 +313,7 @@ public class SDRTrunk implements Listener<TunerEvent>
}
else
{
new ChannelAutoStartFrame(mPlaylistManager.getChannelProcessingManager(), channels);
new ChannelAutoStartFrame(mPlaylistManager.getChannelProcessingManager(), channels, mUserPreferences);
}
}
}

View File

@@ -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,6 +19,7 @@
package io.github.dsheirer.gui.preference;
import io.github.dsheirer.gui.preference.application.ApplicationPreferenceEditor;
import io.github.dsheirer.gui.preference.calibration.VectorCalibrationPreferenceEditor;
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
@@ -39,6 +40,8 @@ public class PreferenceEditorFactory
{
switch(preferenceEditorType)
{
case APPLICATION:
return new ApplicationPreferenceEditor(userPreferences);
case AUDIO_DUPLICATE_CALL_DETECTION:
return new DuplicateCallPreferenceEditor(userPreferences);
case AUDIO_MP3:

View File

@@ -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
@@ -24,6 +24,7 @@ package io.github.dsheirer.gui.preference;
*/
public enum PreferenceEditorType
{
APPLICATION("Application"),
CHANNEL_EVENT("Channel Events"),
DIRECTORY("Directories"),
JMBE_LIBRARY("JMBE Audio Library"),

View File

@@ -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
@@ -165,6 +165,11 @@ public class UserPreferencesEditor extends BorderPane
{
TreeItem<String> treeRoot = new TreeItem<>("Root node");
TreeItem<String> applicationItem = new TreeItem<>("Application");
applicationItem.getChildren().add(new TreeItem(PreferenceEditorType.APPLICATION));
treeRoot.getChildren().add(applicationItem);
applicationItem.setExpanded(true);
TreeItem<String> audioItem = new TreeItem<>("Audio");
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_DUPLICATE_CALL_DETECTION));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_MP3));

View File

@@ -0,0 +1,94 @@
/*
* *****************************************************************************
* 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.preference.application;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.application.ApplicationPreference;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
/**
* Preference settings for application
*/
public class ApplicationPreferenceEditor extends HBox
{
private ApplicationPreference mApplicationPreference;
private GridPane mEditorPane;
private Label mAutoStartTimeoutLabel;
private Spinner<Integer> mTimeoutSpinner;
/**
* Constructs an instance
* @param userPreferences for obtaining reference to preference.
*/
public ApplicationPreferenceEditor(UserPreferences userPreferences)
{
mApplicationPreference = userPreferences.getApplicationPreference();
getChildren().add(getEditorPane());
}
private GridPane getEditorPane()
{
if(mEditorPane == null)
{
int row = 0;
mEditorPane = new GridPane();
mEditorPane.setVgap(10);
mEditorPane.setHgap(10);
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
GridPane.setHalignment(getAutoStartTimeoutLabel(), HPos.RIGHT);
mEditorPane.add(getAutoStartTimeoutLabel(), 0, row);
mEditorPane.add(getTimeoutSpinner(), 1, row);
mEditorPane.add(new Label("seconds"), 2, row);
}
return mEditorPane;
}
private Label getAutoStartTimeoutLabel()
{
if(mAutoStartTimeoutLabel == null)
{
mAutoStartTimeoutLabel = new Label("Channel Auto-Start Timeout");
}
return mAutoStartTimeoutLabel;
}
/**
* Spinner to select channel auto-start timeout value in range 0-30 seconds.
* @return spinner
*/
private Spinner<Integer> getTimeoutSpinner()
{
if(mTimeoutSpinner == null)
{
mTimeoutSpinner = new Spinner<>(0, 30, mApplicationPreference.getChannelAutoStartTimeout(), 1);
mTimeoutSpinner.valueProperty().addListener((observable, oldValue, newValue) -> mApplicationPreference.setChannelAutoStartTimeout(newValue));
}
return mTimeoutSpinner;
}
}

View File

@@ -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
@@ -24,10 +24,9 @@ import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.playback.PlaybackPreference;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
@@ -49,9 +48,6 @@ 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;
/**
@@ -60,20 +56,16 @@ import java.nio.ShortBuffer;
public class PlaybackPreferenceEditor extends HBox
{
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreferenceEditor.class);
private PlaybackPreference mPlaybackPreference;
private final PlaybackPreference mPlaybackPreference;
private GridPane mEditorPane;
private ComboBox<MixerChannelConfiguration> mMixerComboBox;
private Button mMixerTestButton;
private ToggleSwitch mUseAudioSegmentStartToneSwitch;
private Button mTestStartToneButton;
private ToggleSwitch mUseAudioSegmentPreemptToneSwitch;
private Button mTestPreemptToneButton;
private ToggleSwitch mUseAudioSegmentDropToneSwitch;
private Button mTestDropToneButton;
private ComboBox<ToneFrequency> mStartToneFrequencyComboBox;
private ComboBox<ToneVolume> mStartToneVolumeComboBox;
private ComboBox<ToneFrequency> mPreemptToneFrequencyComboBox;
private ComboBox<ToneVolume> mPreemptToneVolumeComboBox;
private ComboBox<ToneFrequency> mDropToneFrequencyComboBox;
private ComboBox<ToneVolume> mDropToneVolumeComboBox;
@@ -114,18 +106,6 @@ public class PlaybackPreferenceEditor extends HBox
mEditorPane.add(getStartToneVolumeComboBox(), 4, row);
mEditorPane.add(getTestStartToneButton(), 5, row);
mEditorPane.add(getUseAudioSegmentPreemptToneSwitch(), 0, ++row);
mEditorPane.add(new Label("Priority Preempt Tone"), 1, row, 3, 1);
Label preemptFrequencyLabel = new Label("Frequency:");
GridPane.setHalignment(preemptFrequencyLabel, HPos.RIGHT);
mEditorPane.add(preemptFrequencyLabel, 1, ++row);
mEditorPane.add(getPreemptToneFrequencyComboBox(), 2, row);
Label preemptVolumeLabel = new Label("Volume:");
GridPane.setHalignment(preemptVolumeLabel, HPos.RIGHT);
mEditorPane.add(preemptVolumeLabel, 3, row);
mEditorPane.add(getPreemptToneVolumeComboBox(), 4, row);
mEditorPane.add(getTestPreemptToneButton(), 5, row);
mEditorPane.add(getUseAudioSegmentDropToneSwitch(), 0, ++row);
mEditorPane.add(new Label("Drop Tone - Do Not Monitor"), 1, row, 3, 1);
Label dropFrequencyLabel = new Label("Frequency:");
@@ -177,52 +157,21 @@ public class PlaybackPreferenceEditor extends HBox
mUseAudioSegmentStartToneSwitch = new ToggleSwitch();
mUseAudioSegmentStartToneSwitch.setAlignment(Pos.BASELINE_RIGHT);
mUseAudioSegmentStartToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentStartTone());
mUseAudioSegmentStartToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
{
mPlaybackPreference.setUseAudioSegmentStartTone(newValue);
}
});
mUseAudioSegmentStartToneSwitch.selectedProperty().addListener((observable, oldValue, newValue) ->
mPlaybackPreference.setUseAudioSegmentStartTone(newValue));
}
return mUseAudioSegmentStartToneSwitch;
}
private ToggleSwitch getUseAudioSegmentPreemptToneSwitch()
{
if(mUseAudioSegmentPreemptToneSwitch == null)
{
mUseAudioSegmentPreemptToneSwitch = new ToggleSwitch();
mUseAudioSegmentPreemptToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentPreemptTone());
mUseAudioSegmentPreemptToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
{
mPlaybackPreference.setUseAudioSegmentPreemptTone(newValue);
}
});
}
return mUseAudioSegmentPreemptToneSwitch;
}
private ToggleSwitch getUseAudioSegmentDropToneSwitch()
{
if(mUseAudioSegmentDropToneSwitch == null)
{
mUseAudioSegmentDropToneSwitch = new ToggleSwitch();
mUseAudioSegmentDropToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentDropTone());
mUseAudioSegmentDropToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
{
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
{
mPlaybackPreference.setUseAudioSegmentDropTone(newValue);
}
});
mUseAudioSegmentDropToneSwitch.selectedProperty().addListener((observable, oldValue, newValue) ->
mPlaybackPreference.setUseAudioSegmentDropTone(newValue));
}
return mUseAudioSegmentDropToneSwitch;
@@ -236,14 +185,7 @@ public class PlaybackPreferenceEditor extends HBox
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setFill(Color.CORNFLOWERBLUE);
mTestStartToneButton.setGraphic(iconNode);
mTestStartToneButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
play(mPlaybackPreference.getStartTone());
}
});
mTestStartToneButton.setOnAction(event -> play(mPlaybackPreference.getStartTone()));
mTestStartToneButton.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
}
@@ -251,29 +193,6 @@ public class PlaybackPreferenceEditor extends HBox
return mTestStartToneButton;
}
public Button getTestPreemptToneButton()
{
if(mTestPreemptToneButton == null)
{
mTestPreemptToneButton = new Button("Test");
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setFill(Color.CORNFLOWERBLUE);
mTestPreemptToneButton.setGraphic(iconNode);
mTestPreemptToneButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
play(mPlaybackPreference.getPreemptTone());
}
});
mTestPreemptToneButton.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
}
return mTestPreemptToneButton;
}
public Button getTestDropToneButton()
{
if(mTestDropToneButton == null)
@@ -282,14 +201,7 @@ public class PlaybackPreferenceEditor extends HBox
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setFill(Color.CORNFLOWERBLUE);
mTestDropToneButton.setGraphic(iconNode);
mTestDropToneButton.setOnAction(new EventHandler<ActionEvent>()
{
@Override
public void handle(ActionEvent event)
{
play(mPlaybackPreference.getDropTone());
}
});
mTestDropToneButton.setOnAction(event -> play(mPlaybackPreference.getDropTone()));
mTestDropToneButton.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not());
}
@@ -357,36 +269,6 @@ public class PlaybackPreferenceEditor extends HBox
return mStartToneVolumeComboBox;
}
public ComboBox<ToneFrequency> getPreemptToneFrequencyComboBox()
{
if(mPreemptToneFrequencyComboBox == null)
{
mPreemptToneFrequencyComboBox = new ComboBox<>();
mPreemptToneFrequencyComboBox.getItems().addAll(ToneFrequency.values());
mPreemptToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneFrequency());
mPreemptToneFrequencyComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneFrequency(newValue));
mPreemptToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
}
return mPreemptToneFrequencyComboBox;
}
public ComboBox<ToneVolume> getPreemptToneVolumeComboBox()
{
if(mPreemptToneVolumeComboBox == null)
{
mPreemptToneVolumeComboBox = new ComboBox<>();
mPreemptToneVolumeComboBox.getItems().addAll(ToneVolume.values());
mPreemptToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneVolume());
mPreemptToneVolumeComboBox.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneVolume(newValue));
mPreemptToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
}
return mPreemptToneVolumeComboBox;
}
/**
* Plays the audio buffer over the default mono playback device
* @param audioSamples with 8 kHz mono PCM samples

View File

@@ -75,6 +75,7 @@ import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
@@ -318,6 +319,19 @@ public class DMRDecoderState extends TimeslotDecoderState
.build();
broadcast(smsEvent);
}
//Unknown Hytera Long Data Service Token Message
else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(packet.getTimestamp())
.eventDescription(DecodeEventType.UNKNOWN_PACKET.name())
.identifiers(mic)
.timeslot(packet.getTimeslot())
.details("HYTERA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
.build();
broadcast(unknownTokenEvent);
}
else
{
DecodeEvent packetEvent = DMRDecodeEvent.builder(packet.getTimestamp())

View File

@@ -32,6 +32,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISPro
import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType;
import io.github.dsheirer.module.decode.ip.UnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
@@ -175,7 +176,7 @@ public class PacketSequenceMessageFactory
}
else
{
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
}

View File

@@ -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
@@ -51,7 +51,7 @@ public class DecodeEventModel extends AbstractTableModel implements Listener<IDe
public static final int COLUMN_FREQUENCY = 8;
public static final int COLUMN_DETAILS = 9;
protected int mMaxMessages = 500;
protected int mMaxMessages = 200;
protected List<IDecodeEvent> mEvents = new ArrayList<>();
protected FilterSet<IDecodeEvent> mEventFilterSet = new DecodeEventFilterSet();

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.event;
@@ -26,13 +23,13 @@ import io.github.dsheirer.filter.FilterSet;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.StuffBitsMessage;
import io.github.dsheirer.sample.Listener;
import javax.swing.table.AbstractTableModel;
import java.awt.EventQueue;
import java.text.SimpleDateFormat;
import java.util.LinkedList;
import java.util.List;
import javax.swing.table.AbstractTableModel;
public class MessageActivityModel extends AbstractTableModel implements Listener<IMessage>
{
private static final long serialVersionUID = 1L;
@@ -41,7 +38,7 @@ public class MessageActivityModel extends AbstractTableModel implements Listener
private static final int TIMESLOT = 2;
private static final int MESSAGE = 3;
protected int mMaxMessages = 500;
protected int mMaxMessages = 200;
protected LinkedList<MessageItem> mMessageItems = new LinkedList<>();
protected int[] mColumnWidths = {20, 20, 500};
protected String[] mHeaders = new String[]{"Time", "Protocol", "Timeslot", "Message"};

View File

@@ -0,0 +1,68 @@
/*
* *****************************************************************************
* 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.module.decode.ip.hytera.sds;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.ip.IPacket;
import java.util.Collections;
import java.util.List;
/**
* Hytera SDS Long Data Message with unknown encoded payload
*/
public class HyteraUnknownPacket implements IPacket
{
private HyteraTokenHeader mHeader;
/**
* Constructs a parser for a header contained within a binary message starting at the offset.
*
* @param header to the packet within the message
*/
public HyteraUnknownPacket(HyteraTokenHeader header)
{
mHeader = header;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("HYTERA UNKNOWN LONG DATA SERVICE TOKEN MESSAGE:").append(getHeader().toString());
return sb.toString();
}
@Override
public HyteraTokenHeader getHeader()
{
return mHeader;
}
@Override
public IPacket getPayload() {return null;}
@Override
public boolean hasPayload() {return false;}
@Override
public List<Identifier> getIdentifiers()
{
return Collections.emptyList();
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.p25.audio;
@@ -42,11 +39,10 @@ import io.github.dsheirer.module.decode.p25.phase1.message.ldu.LDUMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDULinkControlMessage;
import io.github.dsheirer.module.decode.p25.phase1.message.tdu.TDUMessage;
import io.github.dsheirer.preference.UserPreferences;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* P25 Phase 1 IMBE Frame recorder generates P25 call sequence recordings containing JSON representations of audio
* frames, optional encryption and call identifiers.
@@ -55,7 +51,7 @@ public class P25P1CallSequenceRecorder extends MBECallSequenceRecorder
{
private final static Logger mLog = LoggerFactory.getLogger(P25P1CallSequenceRecorder.class);
private static final String PROTOCOL = "APCO25-PHASE1";
public static final String PROTOCOL = "APCO25-PHASE1";
private MBECallSequence mCallSequence;

View File

@@ -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
@@ -24,6 +24,7 @@ package io.github.dsheirer.preference;
*/
public enum PreferenceType
{
APPLICATION,
CALIBRATION,
DECODE_EVENT,
DIRECTORY,

View File

@@ -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,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.preference;
@@ -30,15 +29,14 @@ public enum TimestampFormat
TIMESTAMP_DEFAULT("yyyy-MM-dd HH:mm:ss", "Timestamp: yyyy-MM-dd HH:mm:ss"),
TIMESTAMP_COLONS("yyyy:MM:dd:HH:mm:ss", "Timestamp Colons: yyyy:MM:dd:HH:mm:ss"),
TIMESTAMP_MILLIS("yyyy:MM:dd:HH:mm:ss.SSS", "Timestamp Milliseconds: yyyy:MM:dd:HH:mm:ss.SSS"),
TIMESTAMP_COMPACT("yyyyMMddHHmmss", "Timestamp Compact: yyyyMMddHHmmss"),
TIMESTAMP_COMPACT("yyyyMMdd_HHmmss", "Timestamp Compact: yyyyMMdd_HHmmss"),
DATE_DASHES("yyyy-MM-dd", "Date Dashes: YYYY-MM-DD"),
DATE_COLONS("yyyy:MM:dd", "Date Colons: YYYY:MM:DD"),
DATE_COMPACT("yyyyMMdd", "Date Compact: YYYYMMDD"),
TIME_COMPACT("HHmmss", "Time Compact: HHmmss"),
TIME_COLONS("HH:mm:ss", "Time Colons: HH:mm:ss"),
TIME_MILLIS("HH:mm:ss.SSS", "Time Milliseconds: HH:mm:ss.SSS"),
TIME_SECONDS("ss.SSS", "Time Seconds: ss.SSS"),
;
TIME_SECONDS("ss.SSS", "Time Seconds: ss.SSS");
private String mFormat;
private String mLabel;

View File

@@ -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,6 +20,7 @@
package io.github.dsheirer.preference;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.application.ApplicationPreference;
import io.github.dsheirer.preference.calibration.VectorCalibrationPreference;
import io.github.dsheirer.preference.decoder.JmbeLibraryPreference;
import io.github.dsheirer.preference.directory.DirectoryPreference;
@@ -55,7 +56,7 @@ import io.github.dsheirer.sample.Listener;
*/
public class UserPreferences implements Listener<PreferenceType>
{
private VectorCalibrationPreference mVectorCalibrationPreference;
private ApplicationPreference mApplicationPreference;
private ChannelMultiFrequencyPreference mChannelMultiFrequencyPreference;
private DecodeEventPreference mDecodeEventPreference;
private DirectoryPreference mDirectoryPreference;
@@ -68,6 +69,7 @@ public class UserPreferences implements Listener<PreferenceType>
private RecordPreference mRecordPreference;
private TalkgroupFormatPreference mTalkgroupFormatPreference;
private TunerPreference mTunerPreference;
private VectorCalibrationPreference mVectorCalibrationPreference;
private SwingPreference mSwingPreference = new SwingPreference();
private JavaFxPreferences mJavaFxPreferences = new JavaFxPreferences();
@@ -80,6 +82,19 @@ public class UserPreferences implements Listener<PreferenceType>
loadPreferenceTypes();
}
/**
* Application general/miscellaneous preferences.
* @return application preferences.
*/
public ApplicationPreference getApplicationPreference()
{
return mApplicationPreference;
}
/**
* Vector/SIMD calibration preferences.
* @return vector calibration preferences.
*/
public VectorCalibrationPreference getVectorCalibrationPreference()
{
return mVectorCalibrationPreference;
@@ -203,7 +218,7 @@ public class UserPreferences implements Listener<PreferenceType>
*/
private void loadPreferenceTypes()
{
mVectorCalibrationPreference = new VectorCalibrationPreference(this::receive);
mApplicationPreference = new ApplicationPreference(this::receive);
mChannelMultiFrequencyPreference = new ChannelMultiFrequencyPreference(this::receive);
mDecodeEventPreference = new DecodeEventPreference(this::receive);
mDirectoryPreference = new DirectoryPreference(this::receive);
@@ -216,6 +231,7 @@ public class UserPreferences implements Listener<PreferenceType>
mRecordPreference = new RecordPreference(this::receive);
mTalkgroupFormatPreference = new TalkgroupFormatPreference(this::receive);
mTunerPreference = new TunerPreference(this::receive);
mVectorCalibrationPreference = new VectorCalibrationPreference(this::receive);
}
/**

View File

@@ -0,0 +1,80 @@
/*
* *****************************************************************************
* 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.preference.application;
import io.github.dsheirer.preference.Preference;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
import java.util.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* General/Miscellaneous preferences
*/
public class ApplicationPreference extends Preference
{
private static final String PREFERENCE_KEY_CHANNEL_AUTO_START_TIMEOUT = "channel.auto.start.timeout";
private final static Logger mLog = LoggerFactory.getLogger(ApplicationPreference.class);
private Preferences mPreferences = Preferences.userNodeForPackage(ApplicationPreference.class);
private Integer mChannelAutoStartTimeout;
/**
* Constructs an instance
* @param updateListener to receive notifications that a preference has been updated
*/
public ApplicationPreference(Listener<PreferenceType> updateListener)
{
super(updateListener);
}
@Override
public PreferenceType getPreferenceType()
{
return PreferenceType.APPLICATION;
}
/**
* Channel auto-start timeout. This is the countdown in seconds to allow the user to cancel the channel auto-start.
* @return timeout in seconds.
*/
public int getChannelAutoStartTimeout()
{
if(mChannelAutoStartTimeout == null)
{
mChannelAutoStartTimeout = mPreferences.getInt(PREFERENCE_KEY_CHANNEL_AUTO_START_TIMEOUT, 10);
}
return mChannelAutoStartTimeout;
}
/**
* Sets the channel auto-start timeout seconds value.
* @param timeout in seconds.
*/
public void setChannelAutoStartTimeout(int timeout)
{
mChannelAutoStartTimeout = timeout;
mPreferences.putInt(PREFERENCE_KEY_CHANNEL_AUTO_START_TIMEOUT, timeout);
notifyPreferenceUpdated();
}
}

View File

@@ -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
@@ -25,14 +25,12 @@ import io.github.dsheirer.gui.preference.playback.ToneVolume;
import io.github.dsheirer.preference.Preference;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
import java.util.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.prefs.Preferences;
/**
* User preferences for audio playback
*/
@@ -42,10 +40,6 @@ public class PlaybackPreference extends Preference
private static final String PREFERENCE_KEY_DROP_TONE_FREQUENCY = "audio.playback.segment.drop.frequency";
private static final String PREFERENCE_KEY_DROP_TONE_VOLUME = "audio.playback.segment.drop.volume";
private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE = "audio.playback.segment.preempt.tone";
private static final String PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY = "audio.playback.segment.preempt.frequency";
private static final String PREFERENCE_KEY_PREEMPT_TONE_VOLUME = "audio.playback.segment.preempt.volume";
private static final String PREFERENCE_KEY_USE_AUDIO_SEGMENT_START_TONE = "audio.playback.segment.start.tone";
private static final String PREFERENCE_KEY_START_TONE_FREQUENCY = "audio.playback.segment.start.frequency";
private static final String PREFERENCE_KEY_START_TONE_VOLUME = "audio.playback.segment.start.volume";
@@ -56,12 +50,9 @@ public class PlaybackPreference extends Preference
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreference.class);
private Preferences mPreferences = Preferences.userNodeForPackage(PlaybackPreference.class);
private Boolean mUseAudioSegmentStartTone;
private Boolean mUseAudioSegmentPreemptTone;
private Boolean mUseAudioSegmentDropTone;
private ToneFrequency mStartToneFrequency;
private ToneVolume mStartToneVolume;
private ToneFrequency mPreemptToneFrequency;
private ToneVolume mPreemptToneVolume;
private ToneFrequency mDropToneFrequency;
private ToneVolume mDropToneVolume;
private MixerChannelConfiguration mMixerChannelConfiguration;
@@ -127,29 +118,6 @@ public class PlaybackPreference extends Preference
notifyPreferenceUpdated();
}
/**
* Indicates if an audio segment preempt tone should be used.
*/
public boolean getUseAudioSegmentPreemptTone()
{
if(mUseAudioSegmentPreemptTone == null)
{
mUseAudioSegmentPreemptTone = mPreferences.getBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE, true);
}
return mUseAudioSegmentPreemptTone;
}
/**
* Sets the preference for using an audio segment preempt tone
*/
public void setUseAudioSegmentPreemptTone(boolean use)
{
mUseAudioSegmentPreemptTone = use;
mPreferences.putBoolean(PREFERENCE_KEY_USE_AUDIO_SEGMENT_PREEMPT_TONE, use);
notifyPreferenceUpdated();
}
/**
* Frequency for the drop tone
*/
@@ -198,30 +166,6 @@ public class PlaybackPreference extends Preference
notifyPreferenceUpdated();
}
/**
* Frequency for the preempt tone
*/
public ToneFrequency getPreemptToneFrequency()
{
if(mPreemptToneFrequency == null)
{
int frequency = mPreferences.getInt(PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY, ToneFrequency.F400.getValue());
mPreemptToneFrequency = ToneFrequency.fromValue(frequency);
}
return mPreemptToneFrequency;
}
/**
* Sets the frequency for the preempt tone
*/
public void setPreemptToneFrequency(ToneFrequency toneFrequency)
{
mPreemptToneFrequency = toneFrequency;
mPreferences.putInt(PREFERENCE_KEY_PREEMPT_TONE_FREQUENCY, toneFrequency.getValue());
notifyPreferenceUpdated();
}
/**
* Drop tone volume
*/
@@ -270,30 +214,6 @@ public class PlaybackPreference extends Preference
notifyPreferenceUpdated();
}
/**
* Preempt tone volume
*/
public ToneVolume getPreemptToneVolume()
{
if(mPreemptToneVolume == null)
{
int volume = mPreferences.getInt(PREFERENCE_KEY_PREEMPT_TONE_VOLUME, ToneVolume.V5.getValue());
mPreemptToneVolume = ToneVolume.fromValue(volume);
}
return mPreemptToneVolume;
}
/**
* Sets the preempt tone volume
*/
public void setPreemptToneVolume(ToneVolume toneVolume)
{
mPreemptToneVolume = toneVolume;
mPreferences.putInt(PREFERENCE_KEY_PREEMPT_TONE_VOLUME, toneVolume.getValue());
notifyPreferenceUpdated();
}
/**
* Buffer with samples for the audio segment start tone
*/
@@ -307,19 +227,6 @@ public class PlaybackPreference extends Preference
return null;
}
/**
* Buffer with samples for the audio segment preempt tone
*/
public float[] getPreemptTone()
{
if(getUseAudioSegmentPreemptTone())
{
return ToneUtil.getTone(getPreemptToneFrequency(), getPreemptToneVolume(), TONE_LENGTH_SAMPLES);
}
return null;
}
/**
* Buffer with samples for the audio segment drop tone
*/

View File

@@ -31,8 +31,8 @@ import io.github.dsheirer.record.wave.IRecordingStatusListener;
import io.github.dsheirer.record.wave.NativeBufferWaveRecorder;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.util.StringUtils;
import io.github.dsheirer.util.TimeStamp;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
@@ -57,47 +57,43 @@ public class RecorderFactory
for(RecorderType recorderType: channel.getRecordConfiguration().getRecorders())
{
long frequency = getFrequency(channel);
switch(recorderType)
{
case BASEBAND:
if(channel.isStandardChannel())
{
recorderModules.add(getBasebandRecorder(channel.toString(), userPreferences));
recorderModules.add(getBasebandRecorder(channel.toString(), frequency, userPreferences));
}
break;
case DEMODULATED_BIT_STREAM:
if(channel.isStandardChannel() && channel.getDecodeConfiguration().getDecoderType().providesBitstream())
{
recorderModules.add(new BinaryRecorder(getRecordingBasePath(userPreferences),
getRecordingNameFromChannel(channel), channel.getDecodeConfiguration().getDecoderType().getProtocol()));
StringUtils.replaceIllegalCharacters(channel.toString()),
channel.getDecodeConfiguration().getDecoderType().getProtocol(),
frequency));
}
break;
case TRAFFIC_BASEBAND:
if(channel.isTrafficChannel())
{
recorderModules.add(getBasebandRecorder(channel.toString(), userPreferences));
recorderModules.add(getBasebandRecorder(channel.toString(), frequency, userPreferences));
}
break;
case TRAFFIC_DEMODULATED_BIT_STREAM:
if(channel.isTrafficChannel() && channel.getDecodeConfiguration().getDecoderType().providesBitstream())
{
recorderModules.add(new BinaryRecorder(getRecordingBasePath(userPreferences),
getRecordingNameFromChannel(channel), channel.getDecodeConfiguration().getDecoderType().getProtocol()));
StringUtils.replaceIllegalCharacters(channel.toString()),
channel.getDecodeConfiguration().getDecoderType().getProtocol(),
frequency));
}
break;
case MBE_CALL_SEQUENCE:
if(channel.isStandardChannel() && channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
long frequency = 0;
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
}
else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency)
{
frequency = ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencies().get(0);
}
switch(channel.getDecodeConfiguration().getDecoderType())
{
@@ -119,19 +115,12 @@ public class RecorderFactory
case TRAFFIC_MBE_CALL_SEQUENCE:
if(channel.isTrafficChannel() && channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
long frequency = 0;
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
}
else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency)
{
frequency = ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencies().get(0);
}
switch(channel.getDecodeConfiguration().getDecoderType())
{
case DMR:
recorderModules.add(new DMRCallSequenceRecorder(userPreferences, frequency,
channel.getSystem(), channel.getSite()));
break;
case P25_PHASE1:
recorderModules.add(new P25P1CallSequenceRecorder(userPreferences, frequency,
channel.getSystem(), channel.getSite()));
@@ -150,24 +139,22 @@ public class RecorderFactory
}
/**
* Creates a recording file name from the channel frequency and the channel configuration.
* @param channel to process
* @return recording file name.
* Extract the frequency from the channel configuration
* @param channel with a source configuration
* @return frequency
*/
private static String getRecordingNameFromChannel(Channel channel)
private static long getFrequency(Channel channel)
{
if(channel.getSourceConfiguration() != null)
if(channel.getSourceConfiguration() instanceof SourceConfigTuner)
{
SourceConfiguration sourceConfiguration = channel.getSourceConfiguration();
if(sourceConfiguration instanceof SourceConfigTuner sourceConfigTuner)
{
long frequency = sourceConfigTuner.getFrequency();
return frequency + "_" + channel.toString();
}
return ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
}
else if(channel.getSourceConfiguration() instanceof SourceConfigTunerMultipleFrequency)
{
return ((SourceConfigTunerMultipleFrequency)channel.getSourceConfiguration()).getFrequencies().get(0);
}
return StringUtils.replaceIllegalCharacters(channel.toString());
return 0;
}
/**
@@ -181,11 +168,16 @@ public class RecorderFactory
/**
* Constructs a baseband recorder for use in a processing chain.
*/
public static ComplexSamplesWaveRecorder getBasebandRecorder(String channelName, UserPreferences userPreferences)
public static ComplexSamplesWaveRecorder getBasebandRecorder(String channelName, long frequency, UserPreferences userPreferences)
{
StringBuilder sb = new StringBuilder();
sb.append(getRecordingBasePath(userPreferences));
sb.append(File.separator).append(StringUtils.replaceIllegalCharacters(channelName)).append("_baseband");
sb.append(File.separator);
sb.append(TimeStamp.getTimeStamp("_"));
sb.append("_");
sb.append(frequency);
sb.append("_");
sb.append(StringUtils.replaceIllegalCharacters(channelName)).append("_baseband");
return new ComplexSamplesWaveRecorder(BASEBAND_SAMPLE_RATE, sb.toString());
}

View File

@@ -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
@@ -25,9 +25,6 @@ import io.github.dsheirer.sample.buffer.IByteBufferListener;
import io.github.dsheirer.util.Dispatcher;
import io.github.dsheirer.util.StringUtils;
import io.github.dsheirer.util.TimeStamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
@@ -36,6 +33,8 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Binary recorder module for recording demodulated bit/byte streams
@@ -50,14 +49,14 @@ public class BinaryRecorder extends Module implements IByteBufferListener
private final static Logger mLog = LoggerFactory.getLogger(BinaryRecorder.class);
private static final int MAX_RECORDING_BYTE_SIZE = 524288; //500 kB
private Dispatcher<ByteBuffer> mBufferProcessor = new Dispatcher<>(500,
"sdrtrunk binary recorder", ByteBuffer.allocate(0));
private Dispatcher<ByteBuffer> mBufferProcessor = new Dispatcher<>("sdrtrunk binary recorder", 200, 250);
private AtomicBoolean mRunning = new AtomicBoolean();
private Path mBaseRecordingPath;
private String mRecordingIdentifier;
private BinaryWriter mBinaryWriter = new BinaryWriter();
private int mBytesRecordedCounter;
private Protocol mProtocol;
private long mFrequency;
/**
* Constructs a binary recorder.
@@ -65,13 +64,15 @@ public class BinaryRecorder extends Module implements IByteBufferListener
* @param baseRecordingPath where the recording should be created
* @param recordingIdentifier to include in the recording file name.
* @param protocol to include as a values in the recording file name
* @param frequency in hertz
*/
public BinaryRecorder(Path baseRecordingPath, String recordingIdentifier, Protocol protocol)
public BinaryRecorder(Path baseRecordingPath, String recordingIdentifier, Protocol protocol, long frequency)
{
mBaseRecordingPath = baseRecordingPath;
mRecordingIdentifier = recordingIdentifier;
mBufferProcessor.setListener(mBinaryWriter);
mProtocol = protocol;
mFrequency = frequency;
}
public void start()
@@ -96,7 +97,7 @@ public class BinaryRecorder extends Module implements IByteBufferListener
{
if(mBufferProcessor != null)
{
mBufferProcessor.flushAndStop();
mBufferProcessor.stop();
mBufferProcessor.setListener(null);
try
@@ -122,6 +123,8 @@ public class BinaryRecorder extends Module implements IByteBufferListener
StringBuilder sb = new StringBuilder();
sb.append(TimeStamp.getTimeStamp("_"));
sb.append("_");
sb.append(mFrequency);
sb.append("_");
sb.append(mProtocol.getBitRate()).append("BPS_");
sb.append(mProtocol.getFileNameLabel()).append("_");
sb.append(mRecordingIdentifier.trim());

View File

@@ -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,7 +27,6 @@ import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.Dispatcher;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -45,9 +44,7 @@ public class ComplexSamplesWaveRecorder extends Module implements IComplexSample
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class);
private Dispatcher<ComplexSamples> mBufferProcessor = new Dispatcher<>(500,
"sdrtrunk complex wave recorder", new ComplexSamples(new float[0], new float[0], System.currentTimeMillis()));
private Dispatcher<ComplexSamples> mBufferProcessor = new Dispatcher<>("sdrtrunk complex wave recorder", 100, 250);
private AtomicBoolean mRunning = new AtomicBoolean();
private BufferWaveWriter mWriter;
private String mFilePrefix;
@@ -87,8 +84,6 @@ public class ComplexSamplesWaveRecorder extends Module implements IComplexSample
{
StringBuilder sb = new StringBuilder();
sb.append(mFilePrefix);
sb.append("_");
sb.append(TimeStamp.getTimeStamp("_"));
sb.append(".wav");
mFile = Paths.get(sb.toString());

View File

@@ -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,7 +19,6 @@
package io.github.dsheirer.record.wave;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.ConversionUtils;
import io.github.dsheirer.sample.Listener;
@@ -29,16 +28,16 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.util.Dispatcher;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
/**
* WAVE audio recorder module for recording complex (I&Q) samples to a wave file
@@ -48,8 +47,7 @@ public class NativeBufferWaveRecorder extends Module implements Listener<INative
private static final Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class);
private static final long STATUS_UPDATE_BYTE_INTERVAL = 1_048_576;
private static final long MAX_RECORDING_SIZE = (long)Integer.MAX_VALUE * 2l;
private Dispatcher<INativeBuffer> mBufferProcessor = new Dispatcher<>(500,
"sdrtrunk native buffer wave recorder", new NativeBufferPoisonPill());
private Dispatcher<INativeBuffer> mBufferProcessor = new Dispatcher<>("sdrtrunk native buffer wave recorder", 100, 250);
private AtomicBoolean mRunning = new AtomicBoolean();
private NativeBufferWaveWriter mWriter;

View File

@@ -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,15 +14,10 @@
*
* 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.record.wave;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -35,6 +29,11 @@ import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
public class WaveWriter implements AutoCloseable
{
@@ -115,12 +114,17 @@ public class WaveWriter implements AutoCloseable
{
int version = 2;
while(Files.exists(mFile))
while(Files.exists(mFile) && version < 20)
{
mFile = Paths.get(mFile.toFile().getAbsolutePath().replace(".tmp", "_" + version + ".tmp"));
version++;
}
if(version >= 20)
{
throw new IOException("Unable to create a unique file name for recording - exceeded 20 versioning attempts");
}
mFileChannel = (FileChannel.open(mFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW));
ByteBuffer header = getWaveHeader(mAudioFormat);

View File

@@ -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,13 +18,11 @@
*/
package io.github.dsheirer.sample;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Broadcasts an item to multiple listeners
@@ -32,8 +30,7 @@ import java.util.concurrent.locks.ReentrantLock;
public class Broadcaster<T> implements Listener<T>
{
private final static Logger mLog = LoggerFactory.getLogger(Broadcaster.class);
private ReentrantLock mLock = new ReentrantLock();
private List<Listener<T>> mListeners = new ArrayList<>();
private List<Listener<T>> mListeners = new CopyOnWriteArrayList<>();
public Broadcaster()
{
@@ -90,25 +87,9 @@ public class Broadcaster<T> implements Listener<T>
*/
public void addListener(Listener<T> listener)
{
if(listener != null)
if(listener != null && !mListeners.contains(listener))
{
mLock.lock();
try
{
if(!mListeners.contains(listener))
{
mListeners.add(listener);
}
}
catch(Exception e)
{
mLog.error("Unexpected error while adding listener", e);
}
finally
{
mLock.unlock();
}
mListeners.add(listener);
}
}
@@ -119,20 +100,7 @@ public class Broadcaster<T> implements Listener<T>
{
if(listener != null)
{
mLock.lock();
try
{
mListeners.remove(listener);
}
catch(Exception e)
{
mLog.error("Unexpected error while removing listener", e);
}
finally
{
mLock.unlock();
}
mListeners.remove(listener);
}
}
@@ -149,24 +117,16 @@ public class Broadcaster<T> implements Listener<T>
*/
public void broadcast(T t)
{
mLock.lock();
try
{
List<Listener<T>> listeners = new ArrayList<>(mListeners);
for(Listener listener: listeners)
for(Listener<T> listener: mListeners)
{
listener.receive(t);
}
}
catch(Exception e)
{
mLog.error("Unexpected error while broadcasting to listeners", e);
}
finally
{
mLock.unlock();
mLog.error("Error while broadcasting [" + t.getClass() + "] to listeners");
}
}
}

View File

@@ -52,7 +52,6 @@ public class SourceEvent
REQUEST_FREQUENCY_CHANGE,
REQUEST_FREQUENCY_ROTATION,
REQUEST_FREQUENCY_SELECTION,
REQUEST_SOURCE_DISPOSE,
REQUEST_CHANGE_SQUELCH_THRESHOLD,
REQUEST_CURRENT_SQUELCH_THRESHOLD,
REQUEST_START_SAMPLE_STREAM,
@@ -368,15 +367,6 @@ public class SourceEvent
return new SourceEvent(Event.NOTIFICATION_STOP_SAMPLE_STREAM, source);
}
/**
* Creates a request to dispose of the source. This is normally used for TunerChannelSource to notify the
* parent source manager that the channel is no longer needed
*/
public static SourceEvent sourceDisposeRequest(Source source)
{
return new SourceEvent(Event.REQUEST_SOURCE_DISPOSE, source);
}
/**
* Creates a request to cycle to the next source frequency. This is normally used for decoders to request the
* next frequency in a list when a multiple-frequency source configuration is defined.

View File

@@ -185,9 +185,19 @@ public class TunerFactory
{
//API instance is retained across the lifecycle of the constructed device, so we only close it if we don't get
//a device from it.
SDRplay api = new SDRplay();
SDRplay api = null;
if(api.isAvailable())
try
{
api = new SDRplay();
}
catch(SDRPlayException se)
{
mLog.info("Caught the exception here ...", se);
api = null;
}
if(api != null && api.isAvailable())
{
Device device = api.getDevice(deviceInfo);

View File

@@ -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
@@ -628,19 +628,20 @@ public class AirspyTunerEditor extends TunerEditor<AirspyTuner, AirspyTunerConfi
{
if(hasTuner() && getTuner().getController().isLockedSampleRate())
{
mSampleRateCombo.setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else
{
mSampleRateCombo.setToolTipText("Select a sample rate for the tuner");
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
}
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
private String getTunerInfo()

View File

@@ -19,7 +19,6 @@
package io.github.dsheirer.source.tuner.channel;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
@@ -49,9 +48,9 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
private ComplexMixer mFrequencyCorrectionMixer;
private IRealDecimationFilter mIDecimationFilter;
private IRealDecimationFilter mQDecimationFilter;
private Listener<ComplexSamples> mSamplesListener;
private double mChannelSampleRate;
private long mTunerFrequency;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
/**
* Constructs a frequency translating and CIC decimating channel source.
@@ -68,16 +67,15 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
{
super(producerSourceEventListener, tunerChannel);
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
int desiredDecimation = (int)(sampleRate / channelSpecification.getMinimumSampleRate());
int decimation = DecimationFilterFactory.getDecimationRate(desiredDecimation);
mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation);
mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimation);
mBufferDispatcher = new Dispatcher(BUFFER_MAX_CAPACITY, "sdrtrunk heterodyne channel " +
tunerChannel.getFrequency(), new NativeBufferPoisonPill());
//Set dispatcher to process 1/10 of estimated sample arrival rate, 20 times per second (up to 200% per interval)
mBufferDispatcher = new Dispatcher("sdrtrunk heterodyne channel " + tunerChannel.getFrequency(),
(int)(sampleRate / 10), 50, getHeartbeatManager());
mBufferDispatcher.setListener(new NativeBufferProcessor());
//Setup the frequency mixer to the current source frequency
@@ -91,7 +89,6 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
public void start()
{
super.start();
mStreamHeartbeatProcessor.start();
mBufferDispatcher.start();
}
@@ -100,7 +97,6 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
{
super.stop();
mBufferDispatcher.stop();
mStreamHeartbeatProcessor.stop();
}
@Override
@@ -167,7 +163,7 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
@Override
public void setListener(Listener<ComplexSamples> listener)
{
mStreamHeartbeatProcessor.setListener(listener);
mSamplesListener = listener;
}
@Override
@@ -184,14 +180,29 @@ public class HalfBandTunerChannelSource<T extends INativeBuffer> extends TunerCh
@Override
public void receive(T nativeBuffer)
{
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
if(mSamplesListener != null)
{
ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
mStreamHeartbeatProcessor.receive(new ComplexSamples(i, q, basebanded.timestamp()));
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
{
ComplexSamples basebanded = mFrequencyCorrectionMixer.mix(iterator.next());
float[] i = mIDecimationFilter.decimateReal(basebanded.i());
float[] q = mQDecimationFilter.decimateReal(basebanded.q());
try
{
mSamplesListener.receive(new ComplexSamples(i, q, basebanded.timestamp()));
}
catch(Throwable t)
{
//The listener can be made null and cause the error - only log if we have a non-null listener
if(mSamplesListener != null)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener + "]");
}
}
}
}
}
}

View File

@@ -20,7 +20,6 @@
package io.github.dsheirer.source.tuner.channel;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.ISourceEventListener;
@@ -41,7 +40,7 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
private final static Logger mLog = LoggerFactory.getLogger(PassThroughChannelSource.class);
private TunerController mTunerController;
private Dispatcher<INativeBuffer> mBufferDispatcher;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
private Listener<ComplexSamples> mBufferListener;
/**
* Constructs an instance
@@ -55,17 +54,15 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
{
super(listener, tunerChannel);
mTunerController = tunerController;
mBufferDispatcher = new Dispatcher<>(500, "sdrtrunk pass-through channel " +
tunerChannel.getFrequency(), new NativeBufferPoisonPill());
mBufferDispatcher = new Dispatcher<>("sdrtrunk pass-through channel " + tunerChannel.getFrequency(),
250, 50, getHeartbeatManager());
mBufferDispatcher.setListener(new BufferProcessor());
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
}
@Override
public void start()
{
super.start();
mStreamHeartbeatProcessor.stop();
mBufferDispatcher.start();
}
@@ -74,7 +71,6 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
{
super.stop();
mBufferDispatcher.stop();
mStreamHeartbeatProcessor.stop();
}
@Override
@@ -92,7 +88,7 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
@Override
public void setListener(Listener<ComplexSamples> listener)
{
mStreamHeartbeatProcessor.setListener(listener);
mBufferListener = listener;
}
@Override
@@ -112,11 +108,21 @@ public class PassThroughChannelSource extends TunerChannelSource implements ISou
@Override
public void receive(INativeBuffer nativeBuffer)
{
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
if(mBufferListener != null)
{
mStreamHeartbeatProcessor.receive(iterator.next());
Iterator<ComplexSamples> iterator = nativeBuffer.iterator();
while(iterator.hasNext())
{
try
{
mBufferListener.receive(iterator.next());
}
catch(Throwable t)
{
mLog.error("Error dispatching complex sample buffers to listener [" + mBufferListener + "]", t);
}
}
}
}
}

View File

@@ -1,135 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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.source.tuner.channel;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import io.github.dsheirer.util.ThreadPool;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Thread-safe implementation that coordinates the flow of data/buffer stream processing with a parallel, periodic
* heartbeat using an internal locking mechanism to coordinate the two activities. This class is intended to be used
* by TunerChannelSource implementations to coordinate the flow of sample buffers produced on a dispatch thread with a
* scheduled timer driven heartbeat mechanism, to ensure the downstream listener receives either samples or a
* heartbeat, but not both at the same time.
*
* Samples are processed as they arrive and are immediately followed by a heartbeat. A separate scheduled timer runs
* on a periodic schedule to optionally fire a heartbeat, independent of the sample stream rate. The scheduled
* heartbeat is optional and will skip a heartbeat interval if this processor is currently dispatching a sample buffer.
* Sample stream processing will block until a currently running scheduled heartbeat timer event completes, when these
* two events occur at the same time.
*/
public class StreamProcessorWithHeartbeat<T> implements Listener<T>
{
private ReentrantLock mLock = new ReentrantLock();
private HeartbeatManager mHeartbeatManager;
private long mHeartbeatInterval;
private Listener<T> mListener;
private ScheduledFuture<?> mHeartbeatTimerFuture;
/**
* Constructs an instance
* @param heartbeatManager that will be invoked by an internal scheduled timer
* @param heartbeatInterval in milliseconds
*/
public StreamProcessorWithHeartbeat(HeartbeatManager heartbeatManager, long heartbeatInterval)
{
mHeartbeatManager = heartbeatManager;
mHeartbeatInterval = heartbeatInterval;
}
/**
* Starts the heartbeat timer
*/
public void start()
{
if(mHeartbeatTimerFuture == null)
{
mHeartbeatTimerFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> fireOptionalHeartbeat(),
mHeartbeatInterval, mHeartbeatInterval, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the heartbeat timer
*/
public void stop()
{
if(mHeartbeatTimerFuture != null)
{
mHeartbeatTimerFuture.cancel(true);
mHeartbeatTimerFuture = null;
}
}
/**
* Optionally fires a heartbeat if we're not currently processing a native buffer
*/
private void fireOptionalHeartbeat()
{
//Acquire lock only if it's not already locked, otherwise ignore heartbeat for this timer interval.
if(mLock.tryLock())
{
try
{
mHeartbeatManager.broadcast();
}
finally
{
mLock.unlock();
}
}
}
@Override
public void receive(T data)
{
mLock.lock();
try
{
mHeartbeatManager.broadcast();
Listener<T> listener = mListener;
if(listener != null)
{
listener.receive(data);
}
}
finally
{
mLock.unlock();
}
}
/**
* Registers the listener to receive data/buffers for the defined generic type that are distributed to this processor.
* @param listener to register or null to deregister an existing listener
*/
public void setListener(Listener<T> listener)
{
mListener = listener;
}
}

View File

@@ -109,7 +109,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
public void stop()
{
broadcastProducerSourceEvent(SourceEvent.stopSampleStreamRequest(this));
broadcastProducerSourceEvent(SourceEvent.sourceDisposeRequest(this));
}
@Override

View File

@@ -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.source.tuner.fcd.proV1.FCD1TunerController.MixerGain;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.ui.TunerEditor;
import java.text.DecimalFormat;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +40,6 @@ import javax.swing.JSpinner;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import java.text.DecimalFormat;
/**
* Funcube Dongle Pro tuner editor
@@ -312,7 +312,7 @@ public class FCD1TunerEditor extends TunerEditor<FCDTuner,FCD1TunerConfiguration
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
getFrequencyPanel().updateControls();
}
public JButton getTunerInfoButton()

View File

@@ -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
@@ -195,7 +195,7 @@ public class FCD2TunerEditor extends TunerEditor<FCDTuner, FCD2TunerConfiguratio
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
getFrequencyPanel().updateControls();
}
private String getTunerInfo()

View File

@@ -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
@@ -308,8 +308,9 @@ public class HackRFTunerEditor extends TunerEditor<HackRFTuner,HackRFTunerConfig
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
mSampleRateCombo.setEnabled(!locked);
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
private String getTunerInfo()

View File

@@ -291,6 +291,7 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
{
switch(sourceEvent.getEvent())
{
//TODO: protect start/stop processing with a reentrant lock
case REQUEST_START_SAMPLE_STREAM:
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource)
{
@@ -304,19 +305,13 @@ public class HeterodyneChannelSourceManager extends ChannelSourceManager
}
break;
case REQUEST_STOP_SAMPLE_STREAM:
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource)
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource halfBandSource)
{
mSampleDelayBuffer.removeListener((HalfBandTunerChannelSource)sourceEvent.getSource());
mSampleDelayBuffer.removeListener(halfBandSource);
stopDelayBuffer();
}
break;
case REQUEST_SOURCE_DISPOSE:
if(sourceEvent.getSource() instanceof HalfBandTunerChannelSource)
{
HalfBandTunerChannelSource channelSource = (HalfBandTunerChannelSource) sourceEvent.getSource();
mChannelSources.remove(channelSource);
mTunerChannels.remove(channelSource.getTunerChannel());
channelSource.dispose();
mChannelSources.remove(halfBandSource);
mTunerChannels.remove(halfBandSource.getTunerChannel());
halfBandSource.dispose();
//Unlock the tuner controller if there are no more channels
if(getTunerChannelCount() == 0)

View File

@@ -299,9 +299,18 @@ public class TunerManager implements IDiscoveredTunerStatusListener
//Note: we have to keep this first API instance open while we use any RSP tuners, otherwise the additional API
//instance(s) used by the individual tuners become unresponsive. Note sure why.
mSDRplay = new SDRplay();
if(mSDRplay.isAvailable())
try
{
mSDRplay = new SDRplay();
}
catch(SDRPlayException se)
{
mLog.error("Couldn't load SDRPlay API");
mSDRplay = null;
}
if(mSDRplay != null && mSDRplay.isAvailable())
{
try
{
@@ -333,7 +342,11 @@ public class TunerManager implements IDiscoveredTunerStatusListener
}
else
{
mSDRplay.close();
if(mSDRplay != null)
{
mSDRplay.close();
}
mSDRplay = null;
}
}

View File

@@ -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
@@ -54,7 +54,7 @@ public class RecordingTunerEditor extends TunerEditor<RecordingTuner,RecordingTu
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
getFrequencyPanel().updateControls();
}
@Override

View File

@@ -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
@@ -97,6 +97,6 @@ public class RTL2832UnknownTunerEditor extends TunerEditor<RTL2832Tuner, RTL2832
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
getFrequencyPanel().updateControls();
}
}

View File

@@ -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
@@ -400,8 +400,9 @@ public class E4KTunerEditor extends TunerEditor<RTL2832Tuner, E4KTunerConfigurat
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
mSampleRateCombo.setEnabled(!locked);
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
private String getTunerInfo()

View File

@@ -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
@@ -433,8 +433,9 @@ public class R820TTunerEditor extends TunerEditor<RTL2832Tuner,R820TTunerConfigu
@Override
public void setTunerLockState(boolean locked)
{
super.setTunerLockState(locked);
mSampleRateCombo.setEnabled(!locked);
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
private String getTunerInfo()

View File

@@ -31,6 +31,8 @@ import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.GainReduction;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.IfMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.LoMode;
import io.github.dsheirer.util.ThreadPool;
import java.lang.ref.WeakReference;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,6 +48,7 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
protected int mGain;
private IDeviceEventListener mDeviceEventListener;
private IStreamListener mStreamListener;
protected WeakReference<IGainOverloadListener> mGainOverloadReference;
//Streaming control lock and boolean status indicator. Access to the boolean indicator is protected by the lock.
protected ReentrantLock mStreamingLock = new ReentrantLock();
@@ -77,8 +80,7 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
getDevice().getCompositeParameters().getControlAParameters().getDcOffset().setDC(true);
getDevice().getCompositeParameters().getControlAParameters().getDcOffset().setIQ(true);
//Setup IF, LO, and AGC Mode
getDevice().getCompositeParameters().getControlAParameters().getAgc().setAgcMode(AgcMode.DISABLE);
//Setup IF & LO Mode
getDevice().setIfMode(IfMode.IF_ZERO);
getDevice().setLoMode(LoMode.AUTO);
}
@@ -257,6 +259,20 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
}
}
/**
* Registers the listener to receive gain overload notifications.
* @param listener to register
*/
@Override
public void setGainOverloadListener(IGainOverloadListener listener)
{
if(mGainOverloadReference != null && mGainOverloadReference.get() != null)
{
mGainOverloadReference.clear();
}
mGainOverloadReference = new WeakReference<>(listener);
}
@Override
public void acknowledgePowerOverload(TunerSelect tunerSelect) throws SDRPlayException
@@ -269,6 +285,17 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
{
throw new SDRPlayException("Device is not initialized");
}
//Notify an optional weakly referenced, registered listener that gain overload has been acknowledged.
if(mGainOverloadReference != null)
{
IGainOverloadListener listener = mGainOverloadReference.get();
if(listener != null)
{
ThreadPool.CACHED.submit(() -> listener.notifyGainOverload(tunerSelect));
}
}
}
@Override
@@ -339,6 +366,27 @@ public abstract class ControlRsp<T extends Device> implements IControlRsp
}
}
/**
* Current IF AGC mode setting.
* @return AGC mode.
*/
@Override
public AgcMode getAgcMode()
{
return getDevice().getCompositeParameters().getControlAParameters().getAgc().getAgcMode();
}
/**
* Sets the IF AGC mode
* @param mode to set.
*/
@Override
public void setAgcMode(AgcMode mode) throws SDRPlayException
{
getDevice().getCompositeParameters().getControlAParameters().getAgc().setAgcMode(mode);
getDevice().update(getTunerSelect(), UpdateReason.CONTROL_AGC);
}
/**
* Registers listeners to receive device events and sample streams.
* @param deviceEventListener to receive device events

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.source.tuner.sdrplay.api.callback.IDeviceEventListener
import io.github.dsheirer.source.tuner.sdrplay.api.callback.IStreamListener;
import io.github.dsheirer.source.tuner.sdrplay.api.device.Device;
import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
/**
* Control interface for base RSP device
@@ -125,4 +126,22 @@ public interface IControlRsp
* @return gain index value (0 - 28)
*/
int getGain();
/**
* Current IF AGC mode setting.
* @return IF agc mode
*/
AgcMode getAgcMode();
/**
* Sets the IF AGC mode
* @param mode to set.
*/
void setAgcMode(AgcMode mode) throws SDRPlayException;
/**
* Registers a listener to receive notifications of gain overload.
* @param listener to register
*/
void setGainOverloadListener(IGainOverloadListener listener);
}

View File

@@ -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.source.tuner.sdrplay;
import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
/**
* Listener interface to receive notifications of gain overload in the RSP device.
*/
public interface IGainOverloadListener
{
void notifyGainOverload(TunerSelect tunerSelect);
}

View File

@@ -25,16 +25,16 @@ import java.util.EnumSet;
/**
* RSP device Sample Rate, Bandwidth and Decimation enumeration
*
* Note: final effective sample rate must be greater than IF bandwidth setting to avoid aliasing. The available IF
* bandwidth values effectively dictate the available sample rates
*/
public enum RspSampleRate
{
RATE_0_250(8_000_000, 016_000, Bandwidth.BW_0_300, Decimate.X32, "0.250 MHz (0.234 usable)"),
RATE_0_500(8_000_000, 024_000, Bandwidth.BW_0_600, Decimate.X16, "0.500 MHz (0.476 usable)"),
RATE_0_250(8_000_000, 16_000, Bandwidth.BW_0_300, Decimate.X32, "0.250 MHz (0.234 usable)"),
RATE_0_500(8_000_000, 24_000, Bandwidth.BW_0_600, Decimate.X16, "0.500 MHz (0.476 usable)"),
RATE_1_000(8_000_000, 100_000, Bandwidth.BW_1_536, Decimate.X8, "1.000 MHz (0.900 usable)"),
RATE_1_500(6_000_000, 140_000, Bandwidth.BW_1_536, Decimate.X4, "1.500 MHz (1.360 usable)"),
RATE_2_048(8_192_000, 248_000, Bandwidth.BW_1_536, Decimate.X4, "2.048 MHz (1.800 usable)"),
RATE_3_000(6_000_000, 300_000, Bandwidth.BW_5_000, Decimate.X2, "3.000 MHz (2.700 usable)"),
RATE_4_000(8_000_000, 340_000, Bandwidth.BW_5_000, Decimate.X2, "4.000 MHz (3.560 usable)"),
RATE_5_000(5_000_000, 880_000, Bandwidth.BW_5_000, Decimate.X1, "5.000 MHz (4.120 usable)"),
@@ -55,11 +55,11 @@ public enum RspSampleRate
UNDEFINED(0, 0, Bandwidth.UNDEFINED, Decimate.X1, "UNDEFINED");
private long mSampleRate;
private long mUnusable;
private Bandwidth mBandwidth;
private Decimate mDecimation;
private String mDescription;
private final long mSampleRate;
private final long mUnusable;
private final Bandwidth mBandwidth;
private final Decimate mDecimation;
private final String mDescription;
/**
* Constructs an instance
@@ -81,12 +81,12 @@ public enum RspSampleRate
/**
* Single tuner sample rates for all devices operating in single tuner mode
*/
public static EnumSet<RspSampleRate> SINGLE_TUNER_SAMPLE_RATES = EnumSet.range(RATE_0_250, RATE_10_000);
public static final EnumSet<RspSampleRate> SINGLE_TUNER_SAMPLE_RATES = EnumSet.range(RATE_0_250, RATE_10_000);
/**
* RSPduo dual-tuner mode sample rates
*/
public static EnumSet<RspSampleRate> DUAL_TUNER_SAMPLE_RATES = EnumSet.range(DUO_RATE_0_500, DUO_RATE_2_000);
public static final EnumSet<RspSampleRate> DUAL_TUNER_SAMPLE_RATES = EnumSet.range(DUO_RATE_0_500, DUO_RATE_2_000);
/**
* Sample Rate
@@ -137,14 +137,6 @@ public enum RspSampleRate
return mDecimation;
}
/**
* Indicates if this sample rate specifies decimation
*/
public boolean hasDecimation()
{
return getDecimation().isEnabled();
}
/**
* Effective Sample Rate (sample rate / decimation). Note: for RSPduo operating in dual-tuner mode, an
* inherent x4 decimation is applied in the hardware.

View File

@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.GainReduction;
import io.github.dsheirer.source.tuner.sdrplay.rsp1a.Rsp1aTunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.rsp2.Rsp2TunerConfiguration;
@@ -33,7 +34,7 @@ import io.github.dsheirer.source.tuner.sdrplay.rspDx.RspDxTunerConfiguration;
/**
* Abstract RSP tuner configuration
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Rsp1aTunerConfiguration.class, name = "rsp1aTunerConfiguration"),
@JsonSubTypes.Type(value = Rsp2TunerConfiguration.class, name = "rsp2TunerConfiguration"),
@@ -47,7 +48,8 @@ public abstract class RspTunerConfiguration extends TunerConfiguration
public static final RspSampleRate DEFAULT_DUAL_TUNER_SAMPLE_RATE = RspSampleRate.DUO_RATE_2_000;
private RspSampleRate mRspSampleRate = DEFAULT_SINGLE_TUNER_SAMPLE_RATE;
private int mGain = 14;
private int mGain = 24;
private AgcMode mAgcMode = AgcMode.ENABLE;
/**
* JAXB Constructor
@@ -103,4 +105,23 @@ public abstract class RspTunerConfiguration extends TunerConfiguration
mGain = gain;
}
}
/**
* IF AGC mode
* @return mode
*/
@JacksonXmlProperty(isAttribute = true, localName = "agcMode")
public AgcMode getAgcMode()
{
return mAgcMode;
}
/**
* Sets the IF AGC mode
* @param mode to set
*/
public void setAgcMode(AgcMode mode)
{
mAgcMode = mode;
}
}

View File

@@ -21,11 +21,21 @@ package io.github.dsheirer.source.tuner.sdrplay;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.GainReduction;
import io.github.dsheirer.source.tuner.ui.TunerEditor;
import io.github.dsheirer.util.ThreadPool;
import java.awt.Color;
import java.awt.EventQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JSlider;
@@ -33,11 +43,14 @@ import javax.swing.JSlider;
/**
* Abstract RSP tuner editor
*/
public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends TunerEditor<RspTuner,C>
public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends TunerEditor<RspTuner,C> implements IGainOverloadListener
{
private Logger mLog = LoggerFactory.getLogger(RspTunerEditor.class);
private JSlider mGainSlider;
private JLabel mGainValueLabel;
private JComboBox<AgcMode> mAgcModeCombo;
private JButton mGainOverloadButton;
private AtomicBoolean mGainOverloadAlert = new AtomicBoolean();
/**
* Constructs an instance
@@ -105,4 +118,87 @@ public abstract class RspTunerEditor<C extends RspTunerConfiguration> extends Tu
return mGainValueLabel;
}
/**
* IF AGC mode combobox control
*/
protected JComboBox<AgcMode> getAgcModeCombo()
{
if(mAgcModeCombo == null)
{
mAgcModeCombo = new JComboBox<>(AgcMode.values());
mAgcModeCombo.setEnabled(false);
mAgcModeCombo.addActionListener(e -> {
if(hasTuner() && !isLoading())
{
AgcMode selected = (AgcMode)mAgcModeCombo.getSelectedItem();
try
{
getTunerController().getControlRsp().setAgcMode(selected);
save();
}
catch(SDRPlayException se)
{
mLog.error("Error setting AGC mode on RSP device");
}
save();
}
});
}
return mAgcModeCombo;
}
/**
* Gain overload button. Used in a disabled state to indicate (e.g. flashing color) that gain overload is detected.
*/
protected JButton getGainOverloadButton()
{
if(mGainOverloadButton == null)
{
mGainOverloadButton = new JButton("Gain Overload");
mGainOverloadButton.setToolTipText("Notification that manual gain is set too high and causing power overload. Reduce manual gain when this flashes.");
mGainOverloadButton.setEnabled(false);
}
return mGainOverloadButton;
}
@Override
public void notifyGainOverload(TunerSelect tunerSelect)
{
if(hasTuner() && getTuner().getRspTunerController().getTunerSelect() == tunerSelect)
{
//Set overload alert
EventQueue.invokeLater(() -> setGainOverloadAlert(true));
//Schedule a reset to happen 1 second later
ThreadPool.SCHEDULED.schedule((Runnable) () -> setGainOverloadAlert(false), 600, TimeUnit.MILLISECONDS);
}
}
/**
* Toggles the alert styling of the disabled gain overload button to indicate an alert condition or a normal
* operating condition.
* @param alert true to apply alert styling or false to reset.
*/
private void setGainOverloadAlert(boolean alert)
{
if(alert && mGainOverloadAlert.compareAndSet(false, true))
{
getGainOverloadButton().setEnabled(true);
getGainOverloadButton().setForeground(Color.YELLOW);
getGainOverloadButton().setBackground(Color.RED);
}
else if(!alert && mGainOverloadAlert.compareAndSet(true, false))
{
getGainOverloadButton().setEnabled(false);
getGainOverloadButton().setForeground(getForeground());
getGainOverloadButton().setBackground(getBackground());
}
else
{
mLog.info("Ignoring duplicate gain alerting - alert[" + alert + "] atomic [" + mGainOverloadAlert.get() + "]");
}
}
}

View File

@@ -61,7 +61,8 @@ import org.slf4j.LoggerFactory;
public class SDRplay
{
public static final String SDRPLAY_API_LIBRARY_NAME = "sdrplay_api";
public static final String SDRPLAY_API_PATH_LINUX_AND_OSX = "/usr/local/lib/libsdrplay_api.so";
public static final String SDRPLAY_API_PATH_LINUX = "/usr/local/lib/libsdrplay_api.so";
public static final String SDRPLAY_API_PATH_MAC_OS = "/usr/local/lib/libsdrplay_api.dylib";
public static final String SDRPLAY_API_PATH_WINDOWS = System.getenv("ProgramFiles") +
"\\SDRplay\\API\\" + (System.getProperty("sun.arch.data.model").contentEquals("64") ? "x64" : "x86") +
"\\" + SDRPLAY_API_LIBRARY_NAME;
@@ -103,7 +104,7 @@ public class SDRplay
/**
* Constructs an instance of the SDRPLay API
*/
public SDRplay()
public SDRplay() throws SDRPlayException
{
mSdrplayLibraryLoaded = loadLibrary();
@@ -115,6 +116,11 @@ public class SDRplay
mLog.info("API library - open status: " + openStatus);
}
mAvailable = openStatus.success() && getVersion().isSupported();
if(openStatus == Status.FAIL)
{
throw new SDRPlayException("Service not available");
}
}
else
{
@@ -671,11 +677,11 @@ public class SDRplay
}
else if(SystemUtils.IS_OS_LINUX)
{
return SDRPLAY_API_PATH_LINUX_AND_OSX;
return SDRPLAY_API_PATH_LINUX;
}
else if(SystemUtils.IS_OS_MAC_OSX)
{
return SDRPLAY_API_PATH_LINUX_AND_OSX;
return SDRPLAY_API_PATH_MAC_OS;
}
mLog.error("Unrecognized operating system. Cannot identify sdrplay api library path");
@@ -692,23 +698,23 @@ public class SDRplay
* Java will look there for the library, without having to specify it as a launcher
* option. This would allow for users that have installed the API to an alternate location.
*/
SDRplay sdrplay = new SDRplay();
Status status = sdrplay.open();
mLog.info("Open Status: " + status);
try
{
SDRplay sdrplay = new SDRplay();
Status status = sdrplay.open();
mLog.info("Open Status: " + status);
for(DeviceInfo deviceInfo: sdrplay.getDeviceInfos())
{
mLog.info("Found: " + deviceInfo);
}
Status closeStatus = sdrplay.close();
mLog.info("Close Status: " + closeStatus);
}
catch(SDRPlayException se)
{
mLog.info("Error", se);
}
Status closeStatus = sdrplay.close();
mLog.info("Close Status: " + closeStatus);
}
}

View File

@@ -24,6 +24,7 @@ import io.github.dsheirer.source.tuner.ITunerErrorListener;
import io.github.dsheirer.source.tuner.TunerType;
import io.github.dsheirer.source.tuner.configuration.TunerConfiguration;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerController;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,9 +55,18 @@ public class Rsp1TunerController extends RspTunerController<IControlRsp1>
@Override
public void apply(TunerConfiguration config) throws SourceException
{
if(config instanceof Rsp1TunerConfiguration)
if(config instanceof Rsp1TunerConfiguration rtc)
{
super.apply(config);
try
{
getControlRsp().setAgcMode(rtc.getAgcMode());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSP IF AGC Mode to " + rtc.getAgcMode());
}
}
else
{

View File

@@ -25,12 +25,14 @@ import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.RspSampleRate;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SpinnerNumberModel;
@@ -59,7 +61,7 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
"[][][][][][][][][][][][grow]"));
"[][][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
add(getTunerIdLabel(), "wrap");
@@ -77,6 +79,13 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("IF AGC Mode:"));
JPanel gainPanel = new JPanel();
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
gainPanel.add(getAgcModeCombo());
gainPanel.add(getGainOverloadButton());
add(gainPanel, "wrap");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
@@ -97,6 +106,14 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
return null;
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
@Override
protected void tunerStatusUpdated()
{
@@ -115,6 +132,15 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
getSampleRateCombo().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate());
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
updateSampleRateToolTip();
getAgcModeCombo().setEnabled(hasTuner());
if(hasTuner())
{
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
//Register to receive gain overload notifications
getTunerController().getControlRsp().setGainOverloadListener(this);
}
getGainSlider().setEnabled(hasTuner());
getGainValueLabel().setEnabled(hasTuner());
@@ -134,6 +160,7 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
getConfiguration().setAutoPPMCorrectionEnabled(getAutoPPMCheckBox().isSelected());
getConfiguration().setSampleRate((RspSampleRate)getSampleRateCombo().getSelectedItem());
getConfiguration().setGain(getGainSlider().getValue());
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
saveConfiguration();
}
@@ -169,4 +196,23 @@ public class Rsp1TunerEditor extends RspTunerEditor<Rsp1TunerConfiguration>
return mSampleRateCombo;
}
/**
* Updates the sample rate tooltip according to the tuner controller's lock state.
*/
private void updateSampleRateToolTip()
{
if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
{
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else if(hasTuner())
{
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
else
{
getSampleRateCombo().setToolTipText("No tuner available");
}
}
}

View File

@@ -59,6 +59,15 @@ public class Rsp1aTunerController extends RspTunerController<IControlRsp1a>
{
super.apply(config);
try
{
getControlRsp().setAgcMode(rtc.getAgcMode());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSP IF AGC Mode to " + rtc.getAgcMode());
}
try
{
getControlRsp().setBiasT(rtc.isBiasT());

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.RspSampleRate;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,6 +33,7 @@ import org.slf4j.LoggerFactory;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SpinnerNumberModel;
@@ -63,7 +65,7 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
"[][][][][][][][][][][][grow]"));
"[][][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
add(getTunerIdLabel(), "wrap");
@@ -81,6 +83,13 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("IF AGC Mode:"));
JPanel gainPanel = new JPanel();
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
gainPanel.add(getAgcModeCombo());
gainPanel.add(getGainOverloadButton());
add(gainPanel, "wrap");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
@@ -108,6 +117,14 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
return null;
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
@Override
protected void tunerStatusUpdated()
{
@@ -126,6 +143,15 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
getSampleRateCombo().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate());
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
updateSampleRateToolTip();
getAgcModeCombo().setEnabled(hasTuner());
if(hasTuner())
{
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
//Register to receive gain overload notifications
getTunerController().getControlRsp().setGainOverloadListener(this);
}
getGainSlider().setEnabled(hasTuner());
getGainValueLabel().setEnabled(hasTuner());
@@ -178,6 +204,7 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
getConfiguration().setGain(getGainSlider().getValue());
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
saveConfiguration();
}
@@ -297,4 +324,24 @@ public class Rsp1aTunerEditor extends RspTunerEditor<Rsp1aTunerConfiguration>
return mRfNotchCheckBox;
}
/**
* Updates the sample rate tooltip according to the tuner controller's lock state.
*/
private void updateSampleRateToolTip()
{
if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
{
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else if(hasTuner())
{
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
else
{
getSampleRateCombo().setToolTipText("No tuner available");
}
}
}

View File

@@ -59,6 +59,15 @@ public class Rsp2TunerController extends RspTunerController<IControlRsp2>
{
super.apply(config);
try
{
getControlRsp().setAgcMode(rtc.getAgcMode());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSP IF AGC Mode to " + rtc.getAgcMode());
}
try
{
getControlRsp().setBiasT(rtc.isBiasT());

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.RspSampleRate;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.Rsp2AntennaSelection;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
@@ -33,6 +34,7 @@ import org.slf4j.LoggerFactory;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SpinnerNumberModel;
@@ -65,7 +67,7 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
"[][][][][][][][][][][][grow]"));
"[][][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
add(getTunerIdLabel(), "wrap");
@@ -83,6 +85,13 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("IF AGC Mode:"));
JPanel gainPanel = new JPanel();
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
gainPanel.add(getAgcModeCombo());
gainPanel.add(getGainOverloadButton());
add(gainPanel, "wrap");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
@@ -112,6 +121,14 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
return null;
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
@Override
protected void tunerStatusUpdated()
{
@@ -130,6 +147,15 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
getSampleRateCombo().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate());
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
updateSampleRateToolTip();
getAgcModeCombo().setEnabled(hasTuner());
if(hasTuner())
{
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
//Register to receive gain overload notifications
getTunerController().getControlRsp().setGainOverloadListener(this);
}
getGainSlider().setEnabled(hasTuner());
getGainValueLabel().setEnabled(hasTuner());
@@ -192,6 +218,7 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
getConfiguration().setAntennaSelection((Rsp2AntennaSelection)getAntennaSelectionCombo().getSelectedItem());
getConfiguration().setGain(getGainSlider().getValue());
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
saveConfiguration();
}
@@ -342,4 +369,23 @@ public class Rsp2TunerEditor extends RspTunerEditor<Rsp2TunerConfiguration>
return mAntennaSelectionCombo;
}
/**
* Updates the sample rate tooltip according to the tuner controller's lock state.
*/
private void updateSampleRateToolTip()
{
if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
{
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else if(hasTuner())
{
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
else
{
getSampleRateCombo().setToolTipText("No tuner available");
}
}
}

View File

@@ -20,6 +20,7 @@
package io.github.dsheirer.source.tuner.sdrplay.rspDuo;
import io.github.dsheirer.source.tuner.sdrplay.ControlRsp;
import io.github.dsheirer.source.tuner.sdrplay.IGainOverloadListener;
import io.github.dsheirer.source.tuner.sdrplay.RspSampleRate;
import io.github.dsheirer.source.tuner.sdrplay.api.DeviceSelectionMode;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
@@ -28,11 +29,11 @@ import io.github.dsheirer.source.tuner.sdrplay.api.UpdateReason;
import io.github.dsheirer.source.tuner.sdrplay.api.device.RspDuoDevice;
import io.github.dsheirer.source.tuner.sdrplay.api.device.RspDuoTuner;
import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.ControlParameters;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.IfMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.LoMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.TunerParameters;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -97,8 +98,7 @@ public abstract class ControlRspDuo<T extends RspDuoTuner> extends ControlRsp<Rs
getControlParameters().getDcOffset().setDC(true);
getControlParameters().getDcOffset().setIQ(true);
//Setup LO and AGC Mode
getControlParameters().getAgc().setAgcMode(AgcMode.DISABLE);
//Setup LO Mode
getTunerParameters().setLoMode(LoMode.AUTO);
//Setup IF mode. In master/slave dual-tuner mode use IF_2048, otherwise use IF_ZERO
@@ -239,6 +239,17 @@ public abstract class ControlRspDuo<T extends RspDuoTuner> extends ControlRsp<Rs
{
throw new SDRPlayException("Device is not initialized");
}
//Notify an optional weakly referenced, registered listener that gain overload has been acknowledged.
if(mGainOverloadReference != null)
{
IGainOverloadListener listener = mGainOverloadReference.get();
if(listener != null)
{
ThreadPool.CACHED.submit(() -> listener.notifyGainOverload(tunerSelect));
}
}
}
@Override

View File

@@ -20,9 +20,11 @@
package io.github.dsheirer.source.tuner.sdrplay.rspDuo;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.UpdateReason;
import io.github.dsheirer.source.tuner.sdrplay.api.device.RspDuoDevice;
import io.github.dsheirer.source.tuner.sdrplay.api.device.RspDuoTuner2;
import io.github.dsheirer.source.tuner.sdrplay.api.device.TunerSelect;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.ControlParameters;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.TunerParameters;
import org.slf4j.Logger;
@@ -103,4 +105,25 @@ public abstract class ControlRspDuoTuner2 extends ControlRspDuo<RspDuoTuner2> im
throw new SDRPlayException("Device is not initialized");
}
}
/**
* Current IF AGC mode setting.
* @return AGC mode.
*/
@Override
public AgcMode getAgcMode()
{
return getDevice().getCompositeParameters().getControlBParameters().getAgc().getAgcMode();
}
/**
* Sets the IF AGC mode
* @param mode to set.
*/
@Override
public void setAgcMode(AgcMode mode) throws SDRPlayException
{
getDevice().getCompositeParameters().getControlBParameters().getAgc().setAgcMode(mode);
getDevice().update(getTunerSelect(), UpdateReason.CONTROL_AGC);
}
}

View File

@@ -57,53 +57,62 @@ public class RspDuoTuner1Controller extends RspTunerController<IControlRspDuoTun
@Override
public void apply(TunerConfiguration config) throws SourceException
{
if(config instanceof RspDuoTuner1Configuration duo1)
if(config instanceof RspDuoTuner1Configuration rtc)
{
super.apply(config);
try
{
getControlRsp().setRfNotch(duo1.isRfNotch());
getControlRsp().setAgcMode(rtc.getAgcMode());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 1 RF notch enabled to " + duo1.isRfNotch());
mLog.error("Error setting RSP IF AGC Mode to " + rtc.getAgcMode());
}
try
{
getControlRsp().setRfDabNotch(duo1.isRfDabNotch());
getControlRsp().setRfNotch(rtc.isRfNotch());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 1 RF DAB notch enabled to " + duo1.isRfDabNotch());
mLog.error("Error setting RSPduo tuner 1 RF notch enabled to " + rtc.isRfNotch());
}
try
{
getControlRsp().setAmNotch(duo1.isAmNotch());
getControlRsp().setRfDabNotch(rtc.isRfDabNotch());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 1 AM notch enabled to " + duo1.isAmNotch());
mLog.error("Error setting RSPduo tuner 1 RF DAB notch enabled to " + rtc.isRfDabNotch());
}
try
{
getControlRsp().setAmPort(duo1.getAmPort());
getControlRsp().setAmNotch(rtc.isAmNotch());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 1 AM port to " + duo1.getAmPort());
mLog.error("Error setting RSPduo tuner 1 AM notch enabled to " + rtc.isAmNotch());
}
try
{
getControlRsp().setExternalReferenceOutput(duo1.isExternalReferenceOutput());
getControlRsp().setAmPort(rtc.getAmPort());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 1 external reference output enabled to " + duo1.isExternalReferenceOutput());
mLog.error("Error setting RSPduo tuner 1 AM port to " + rtc.getAmPort());
}
try
{
getControlRsp().setExternalReferenceOutput(rtc.isExternalReferenceOutput());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 1 external reference output enabled to " + rtc.isExternalReferenceOutput());
}
}
else

View File

@@ -28,6 +28,7 @@ import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.RspSampleRate;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.RspDuoAmPort;
import java.util.EnumSet;
import net.miginfocom.swing.MigLayout;
@@ -73,7 +74,7 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
"[][][][][][][][][][][][][][][grow]"));
"[][][][][][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
JPanel labelAndButtonPanel = new JPanel();
@@ -93,6 +94,13 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("IF AGC Mode:"));
JPanel gainPanel = new JPanel();
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
gainPanel.add(getAgcModeCombo());
gainPanel.add(getGainOverloadButton());
add(gainPanel, "wrap");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
@@ -124,6 +132,14 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
return null;
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
@Override
protected void tunerStatusUpdated()
{
@@ -138,6 +154,8 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
}
getTunerStatusLabel().setText(status);
getButtonPanel().updateControls();
mLog.info("Update tuner status - has tuner: " + hasTuner() + " is locked:" + getTuner().getTunerController().isLockedSampleRate());
getFrequencyPanel().updateControls();
clearSampleRates();
@@ -147,6 +165,15 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
}
getSampleRateCombo().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate());
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
updateSampleRateToolTip();
getAgcModeCombo().setEnabled(hasTuner());
if(hasTuner())
{
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
//Register to receive gain overload notifications
getTunerController().getControlRsp().setGainOverloadListener(this);
}
getGainSlider().setEnabled(hasTuner());
getGainValueLabel().setEnabled(hasTuner());
@@ -222,6 +249,7 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
getConfiguration().setGain(getGainSlider().getValue());
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
saveConfiguration();
}
@@ -438,4 +466,23 @@ public class RspDuoTuner1Editor extends RspTunerEditor<RspDuoTuner1Configuration
return mTunerPreferencesButton;
}
/**
* Updates the sample rate tooltip according to the tuner controller's lock state.
*/
private void updateSampleRateToolTip()
{
if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
{
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else if(hasTuner())
{
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
else
{
getSampleRateCombo().setToolTipText("No tuner available");
}
}
}

View File

@@ -57,44 +57,56 @@ public class RspDuoTuner2Controller extends RspTunerController<IControlRspDuoTun
@Override
public void apply(TunerConfiguration config) throws SourceException
{
if(config instanceof RspDuoTuner2Configuration duo2)
if(config instanceof RspDuoTuner2Configuration rtc)
{
super.apply(config);
try
if(!getControlRsp().isSlaveMode())
{
getControlRsp().setRfNotch(duo2.isRfNotch());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 2 RF notch enabled to " + duo2.isRfNotch());
try
{
getControlRsp().setAgcMode(rtc.getAgcMode());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSP IF AGC Mode to " + rtc.getAgcMode());
}
}
try
{
getControlRsp().setRfDabNotch(duo2.isRfDabNotch());
getControlRsp().setRfNotch(rtc.isRfNotch());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 2 RF DAB notch enabled to " + duo2.isRfDabNotch());
mLog.error("Error setting RSPduo tuner 2 RF notch enabled to " + rtc.isRfNotch());
}
try
{
getControlRsp().setBiasT(duo2.isBiasT());
getControlRsp().setRfDabNotch(rtc.isRfDabNotch());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 2 Bias-T enabled to " + duo2.isBiasT());
mLog.error("Error setting RSPduo tuner 2 RF DAB notch enabled to " + rtc.isRfDabNotch());
}
try
{
getControlRsp().setExternalReferenceOutput(duo2.isExternalReferenceOutput());
getControlRsp().setBiasT(rtc.isBiasT());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 2 external reference output enabled to " + duo2.isExternalReferenceOutput());
mLog.error("Error setting RSPduo tuner 2 Bias-T enabled to " + rtc.isBiasT());
}
try
{
getControlRsp().setExternalReferenceOutput(rtc.isExternalReferenceOutput());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSPduo tuner 2 external reference output enabled to " + rtc.isExternalReferenceOutput());
}
}
else

View File

@@ -28,6 +28,7 @@ import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.RspSampleRate;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import java.util.EnumSet;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
@@ -71,7 +72,7 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
private void init()
{
setLayout(new MigLayout("fill,wrap 3", "[right][grow,fill][fill]",
"[][][][][][][][][][][][][][][grow]"));
"[][][][][][][][][][][][][][][][grow]"));
add(new JLabel("Tuner:"));
JPanel labelAndButtonPanel = new JPanel();
@@ -91,6 +92,13 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("IF AGC Mode:"));
JPanel gainPanel = new JPanel();
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
gainPanel.add(getAgcModeCombo());
gainPanel.add(getGainOverloadButton());
add(gainPanel, "wrap");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
@@ -133,6 +141,14 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
return getDiscoveredRspTuner().getDeviceInfo().getDeviceSelectionMode().isSlaveMode();
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
@Override
protected void tunerStatusUpdated()
{
@@ -164,6 +180,15 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
}
getSampleRateCombo().setEnabled(hasTuner() && !isSlaveMode() && !getTuner().getTunerController().isLockedSampleRate());
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
updateSampleRateToolTip();
getAgcModeCombo().setEnabled(hasTuner());
if(hasTuner())
{
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
//Register to receive gain overload notifications
getTunerController().getControlRsp().setGainOverloadListener(this);
}
getGainSlider().setEnabled(hasTuner());
getGainValueLabel().setEnabled(hasTuner());
@@ -234,6 +259,7 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
getConfiguration().setRfDabNotch(getRfDabNotchCheckBox().isSelected());
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
getConfiguration().setGain(getGainSlider().getValue());
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
saveConfiguration();
}
@@ -420,4 +446,23 @@ public class RspDuoTuner2Editor extends RspTunerEditor<RspDuoTuner2Configuration
return mTunerPreferencesButton;
}
/**
* Updates the sample rate tooltip according to the tuner controller's lock state.
*/
private void updateSampleRateToolTip()
{
if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
{
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else if(hasTuner())
{
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
else
{
getSampleRateCombo().setToolTipText("No tuner available");
}
}
}

View File

@@ -59,6 +59,15 @@ public class RspDxTunerController extends RspTunerController<IControlRspDx>
{
super.apply(config);
try
{
getControlRsp().setAgcMode(rtc.getAgcMode());
}
catch(SDRPlayException se)
{
mLog.error("Error setting RSP IF AGC Mode to " + rtc.getAgcMode());
}
try
{
getControlRsp().setAntenna(rtc.getAntenna());

View File

@@ -25,6 +25,7 @@ import io.github.dsheirer.source.tuner.sdrplay.DiscoveredRspTuner;
import io.github.dsheirer.source.tuner.sdrplay.RspSampleRate;
import io.github.dsheirer.source.tuner.sdrplay.RspTunerEditor;
import io.github.dsheirer.source.tuner.sdrplay.api.SDRPlayException;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.control.AgcMode;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.HdrModeBandwidth;
import io.github.dsheirer.source.tuner.sdrplay.api.parameter.tuner.RspDxAntenna;
import net.miginfocom.swing.MigLayout;
@@ -34,6 +35,7 @@ import org.slf4j.LoggerFactory;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.SpinnerNumberModel;
@@ -86,6 +88,13 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
add(new JLabel("Sample Rate:"));
add(getSampleRateCombo(), "wrap");
add(new JLabel("IF AGC Mode:"));
JPanel gainPanel = new JPanel();
gainPanel.setLayout(new MigLayout("insets 0","[grow,fill][]",""));
gainPanel.add(getAgcModeCombo());
gainPanel.add(getGainOverloadButton());
add(gainPanel, "wrap");
add(new JLabel("Gain:"));
add(getGainSlider());
add(getGainValueLabel());
@@ -121,6 +130,14 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
return null;
}
@Override
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
getSampleRateCombo().setEnabled(!locked);
updateSampleRateToolTip();
}
@Override
protected void tunerStatusUpdated()
{
@@ -139,6 +156,15 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
getSampleRateCombo().setEnabled(hasTuner() && !getTuner().getTunerController().isLockedSampleRate());
getSampleRateCombo().setSelectedItem(hasTuner() ? getTunerController().getControlRsp().getSampleRateEnumeration() : null);
updateSampleRateToolTip();
getAgcModeCombo().setEnabled(hasTuner());
if(hasTuner())
{
getAgcModeCombo().setSelectedItem(getTunerController().getControlRsp().getAgcMode());
//Register to receive gain overload notifications
getTunerController().getControlRsp().setGainOverloadListener(this);
}
getGainSlider().setEnabled(hasTuner());
getGainValueLabel().setEnabled(hasTuner());
@@ -213,6 +239,7 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
getConfiguration().setRfNotch(getRfNotchCheckBox().isSelected());
getConfiguration().setAntenna((RspDxAntenna) getAntennaCombo().getSelectedItem());
getConfiguration().setGain(getGainSlider().getValue());
getConfiguration().setAgcMode((AgcMode)getAgcModeCombo().getSelectedItem());
saveConfiguration();
}
@@ -420,4 +447,23 @@ public class RspDxTunerEditor extends RspTunerEditor<RspDxTunerConfiguration>
return mHdrModeBandwidthCombo;
}
/**
* Updates the sample rate tooltip according to the tuner controller's lock state.
*/
private void updateSampleRateToolTip()
{
if(hasTuner() && getTuner().getTunerController().isLockedSampleRate())
{
getSampleRateCombo().setToolTipText("Sample Rate is locked. Disable decoding channels to unlock.");
}
else if(hasTuner())
{
getSampleRateCombo().setToolTipText("Select a sample rate for the tuner");
}
else
{
getSampleRateCombo().setToolTipText("No tuner available");
}
}
}

View File

@@ -547,12 +547,11 @@ public abstract class TunerEditor<T extends Tuner,C extends TunerConfiguration>
/**
* Sets the lock state for the tuner so that the frequency and sample rate controls can be enabled/disabled.
*
* Note: implementing classes should invoke: getFrequencyPanel().updateControls() method.
*
* @param locked true if the tuner is locked.
*/
public void setTunerLockState(boolean locked)
{
getFrequencyPanel().updateControls();
}
public abstract void setTunerLockState(boolean locked);
/**
* Tuner buttons panel

View File

@@ -699,23 +699,23 @@ public abstract class USBTunerController extends TunerController
case LibUsb.TRANSFER_STALL:
case LibUsb.TRANSFER_TIMED_OUT:
case LibUsb.TRANSFER_ERROR:
//Note: cancel flag can be set by libusb, independent of commanded cancel of transfers - we simply
//resubmit the transfer for continued use.
case LibUsb.TRANSFER_CANCELLED:
int transferLength = transfer.actualLength();
if(transferLength > 0)
{
dispatchTransfer(transfer);
transfer.buffer().rewind();
}
transfer.buffer().rewind();
if(mAutoResubmitTransfers)
{
submitTransfer(transfer);
}
break;
case LibUsb.TRANSFER_CANCELLED:
//Reset the transfer but don't do anything else since we're shutting down
transfer.buffer().rewind();
break;
default:
//Unexpected transfer error - shutdown the tuner
transfer.buffer().rewind();

View File

@@ -46,7 +46,7 @@ public class ComplexDftProcessor<T extends INativeBuffer> implements Listener<T>
private static final String FRAME_RATE_PROPERTY = "spectral.display.frame.rate";
//The Cosine and Hann windows seem to offer the best spectral display with minimal bin leakage/smearing
private WindowType mWindowType = WindowType.HANN;
private WindowType mWindowType = WindowType.BLACKMAN_HARRIS_7;
private float[] mWindow;
private DFTSize mDFTSize = DFTSize.FFT04096;
private DFTSize mNewDFTSize = DFTSize.FFT04096;

View File

@@ -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,52 +18,70 @@
*/
package io.github.dsheirer.util;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.sample.Listener;
import java.util.concurrent.LinkedBlockingQueue;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Threaded processor for receiving elements from a separate producer thread and forwarding those buffers to a
* registered listener on this consumer/dispatcher thread.
* Threaded scheduled processor for receiving elements from a separate producer thread and forwarding those buffers to a
* registered listener on this consumer/dispatcher thread. Internally uses a single-thread thread pool to effect a
* timer-based interval for processing to avoid excessive context switching inherent in a blocking queue. Sizes the
* thread pool to a single thread to ensure Garbage Collector can efficiently clean objects created on the thread.
*/
public class Dispatcher<E> implements Listener<E>
{
private final static Logger mLog = LoggerFactory.getLogger(Dispatcher.class);
private static final long OVERFLOW_LOG_EVENT_WAIT_PERIOD = TimeUnit.SECONDS.toMillis(10);
private LinkedBlockingQueue<E> mQueue;
private LinkedTransferQueue<E> mQueue = new LinkedTransferQueue<>();
private Listener<E> mListener;
private AtomicBoolean mRunning = new AtomicBoolean();
private String mThreadName;
private Thread mThread;
private E mPoisonPill;
private long mLastOverflowLogEvent;
private ScheduledExecutorService mExecutorService;
private ScheduledFuture<?> mScheduledFuture;
private int mBatchSize;
private long mInterval;
private HeartbeatManager mHeartbeatManager;
/**
* Constructs an instance
* @param maxSize of the internal queue
* Constructs an instance of a Dispatcher with integrated heartbeat support.
* @param threadName to name the dispatcher thread
* @param poisonPill of type E, used to kill the thread
* @param batchSize is maximum number of elements to process from the queue per interval. This should be sized to
* the anticipated number of elements per second, divided by the number of intervals per second and increased by
* a factor of two. This serves to constrain how many elements are processed per interval when the queue starts
* to back up, so that the thread doesn't dominate CPU resources.
* @param interval for processing each batch in milliseconds.
* @param heartbeatManager to receive a heartbeat command at each processing interval.
*/
public Dispatcher(int maxSize, String threadName, E poisonPill)
public Dispatcher(String threadName, int batchSize, long interval, HeartbeatManager heartbeatManager)
{
if(poisonPill == null)
{
throw new IllegalArgumentException("Poison pill must be non-null");
}
mQueue = new LinkedBlockingQueue<>(maxSize);
mThreadName = threadName;
mPoisonPill = poisonPill;
this(threadName, batchSize, interval);
mHeartbeatManager = heartbeatManager;
}
/**
* Listener to receive the queued buffers each time this processor runs.
* Constructs an instance
* @param threadName to name the dispatcher thread
* @param batchSize is maximum number of elements to process from the queue per interval. This should be sized to
* the anticipated number of elements per second, divided by the number of intervals per second and increased by
* a factor of two. This serves to constrain how many elements are processed per interval when the queue starts
* to back up, so that the thread doesn't dominate CPU resources.
* @param interval for processing each batch in milliseconds.
*/
protected Listener<E> getListener()
public Dispatcher(String threadName, int batchSize, long interval)
{
return mListener;
mThreadName = threadName;
mBatchSize = batchSize;
mInterval = interval;
}
/**
@@ -86,14 +104,7 @@ public class Dispatcher<E> implements Listener<E>
{
if(mRunning.get())
{
if(!mQueue.offer(e))
{
if(System.currentTimeMillis() > (mLastOverflowLogEvent + OVERFLOW_LOG_EVENT_WAIT_PERIOD))
{
mLastOverflowLogEvent = System.currentTimeMillis();
mLog.warn("Temporary buffer overflow for thread [" + mThreadName + "] - throwing away samples");
}
}
mQueue.add(e);
}
}
@@ -104,10 +115,22 @@ public class Dispatcher<E> implements Listener<E>
{
if(mRunning.compareAndSet(false, true))
{
mThread = new Thread(new Processor());
mThread.setName(mThreadName);
mThread.setPriority(Thread.MAX_PRIORITY);
mThread.start();
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
}
if(mExecutorService != null)
{
mExecutorService.shutdown();
mExecutorService = null;
}
mQueue.clear();
mExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(mThreadName));
Runnable r = (mHeartbeatManager != null ? new ProcessorWithHeartbeat() : new Processor());
mScheduledFuture = mExecutorService.scheduleAtFixedRate(r, 0, mInterval, TimeUnit.MILLISECONDS);
}
}
@@ -118,29 +141,21 @@ public class Dispatcher<E> implements Listener<E>
{
if(mRunning.compareAndSet(true, false))
{
mQueue.offer(mPoisonPill);
try
if(mScheduledFuture != null)
{
mThread.interrupt();
mThread.join();
mThread = null;
mScheduledFuture.cancel(true);
mScheduledFuture = null;
mQueue.clear();
}
catch(Exception e)
if(mExecutorService != null)
{
mLog.error("Timeout while waiting to join terminating buffer processor thread");
mExecutorService.shutdown();
mExecutorService = null;
}
}
}
/**
* Finishes processing any queued buffers and then stops the processing thread.
*/
public void flushAndStop()
{
mQueue.offer(mPoisonPill);
}
/**
* Indicates if this processor is currently running
*/
@@ -149,55 +164,75 @@ public class Dispatcher<E> implements Listener<E>
return mRunning.get();
}
/**
* Processes elements from the queue. Note: this should only be invoked on the Processor thread.
*/
private void process()
{
List<E> elements = new ArrayList<>();
mQueue.drainTo(elements, mBatchSize);
for(E element: elements)
{
if(mRunning.get() && mListener != null)
{
try
{
mListener.receive(element);
}
catch(Throwable t)
{
mLog.error("Error while dispatching element [" + element.getClass() + "] to listener [" +
mListener.getClass() + "]", t);
}
}
}
}
/**
* Processor to service the buffer queue and distribute the buffers to the registered listener
*/
class Processor implements Runnable
{
private AtomicBoolean mRunning = new AtomicBoolean();
@Override
public void run()
{
try
if(mRunning.compareAndSet(false, true))
{
mQueue.clear();
process();
mRunning.set(false);
}
}
}
E element;
/**
* Processor to service the buffer queue and distribute the buffers to the registered listener. Includes a
* support for commanding a heart beat with each processing interval.
*/
class ProcessorWithHeartbeat implements Runnable
{
private AtomicBoolean mRunning = new AtomicBoolean();
while(mRunning.get())
@Override
public void run()
{
if(mRunning.compareAndSet(false, true))
{
process();
try
{
try
{
element = mQueue.take();
if(mPoisonPill == element)
{
mRunning.set(false);
}
else if(element != null)
{
if(mListener == null)
{
throw new IllegalStateException("Listener for [" + mThreadName + "] is null");
}
mListener.receive(element);
}
}
catch(InterruptedException e)
{
//Normal shutdown is by interrupt
}
catch(Exception e)
{
mLog.error("Error while processing element", e);
}
mHeartbeatManager.broadcast();
}
catch(Throwable t)
{
mLog.error("Error broadcasting heartbeat during Dispatcher processing interval", t);
}
//Shutting down - clear the queue
mQueue.clear();
}
catch(Throwable t)
{
mLog.error("Unexpected error thrown from the Dispatcher thread", t);
mRunning.set(false);
}
}
}