mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 07:37:25 -05:00
Compare commits
15 Commits
v0.6.0-alp
...
v0.5.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59e74d70b4 | ||
|
|
8d6b7c3ac8 | ||
|
|
372d2ff870 | ||
|
|
32a8363b45 | ||
|
|
87f1f8986f | ||
|
|
b3c08053fb | ||
|
|
a5208dcf1e | ||
|
|
2782953518 | ||
|
|
366055145e | ||
|
|
39ebdb47b1 | ||
|
|
c788292db8 | ||
|
|
f0dc80437c | ||
|
|
2d6c5c2331 | ||
|
|
bc4e6cb5b0 | ||
|
|
d69ad46c10 |
38
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal 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.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
16
.github/ISSUE_TEMPLATE/how-to-question-or-does-the-software-support-xxx.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/how-to-question-or-does-the-software-support-xxx.md
vendored
Normal 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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -313,7 +313,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
}
|
||||
else
|
||||
{
|
||||
new ChannelAutoStartFrame(mPlaylistManager.getChannelProcessingManager(), channels);
|
||||
new ChannelAutoStartFrame(mPlaylistManager.getChannelProcessingManager(), channels, mUserPreferences);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,6 @@ public abstract class TunerChannelSource extends ComplexSource implements ISourc
|
||||
public void stop()
|
||||
{
|
||||
broadcastProducerSourceEvent(SourceEvent.stopSampleStreamRequest(this));
|
||||
broadcastProducerSourceEvent(SourceEvent.sourceDisposeRequest(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user