Compare commits

...

15 Commits

Author SHA1 Message Date
Denny Sheirer
5b6377cc11 #1558 Resolves issue where heterodyne channelizer allows tuning a set of channels that exceed the tuner's bandwidth. (#1561)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-21 06:12:11 -04:00
Denny Sheirer
9502ca3069 #1547 Updates playlist alias editor to make add/delete operations more efficient. (#1560)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-21 04:54:26 -04:00
Denny Sheirer
77629000b2 #1548 Reduces default decoder AF gain by 40% to resolve issue with distorted audio from local strong aircraft transmissions. (#1559)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-21 04:16:51 -04:00
Nokoa
ca7916f992 added Rsp1TunerConfiguration to JsonSubTypes of RspTunerConfiguration (#1555)
Following reports that RSP1 tuner configurations are reset to default on startup. Unable to test
2023-05-20 07:45:05 -04:00
Denny Sheirer
9de83843a3 #1556 DMR Capacity Plus parsing enhancements. (#1557)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-20 07:44:07 -04:00
tadscottsmith
cbab7bb0c9 Adds native Rdio Scanner streaming. (#1) (#1476)
#1476 Adds rdio-scanner call upload API streaming support
---------

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-15 05:28:07 -04:00
wedgef5
3df8d60eed #1507 Resolves issue with Cap+ LSN parsing. Updates Cap+ Site Status and adds RAS bit handling. (#1536)
Co-authored-by: Matthew Foster <matthew.foster@noaa.gov>
2023-05-14 08:03:49 -04:00
Denny Sheirer
b39dd1a19f #648 Resolves issue with audio panel sometimes displaying stale metadata that doesn't clear until the next audio segment is loaded. (#1545)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-10 04:29:46 -04:00
Denny Sheirer
0af5963d08 #1528 Restores consistent audio segment start tone insertion, broken in #1506 (#1543)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-08 03:59:42 -04:00
Denny Sheirer
4f8b8e978b #35 Updates AM & NBFM decoders to use Adaptive Squelch, a power squelch that includes an adaptive noise floor monitor. Implements vectorized magnitude operations for AM & FM decoders. Adds talkgroup assignment and squelch controls to AM channel configuration editor. Implements new AM gain control that attempts to normalize audio gain quickly at beginning of call/transmission and maintains that gain across lifecycle of the call and resets the gain for the next call. (#1540)
Notes on usage:
- When using either AM or FM squelch control use either the Heterodyne channelizer or if using the Polyphase channelizer, avoid using trunk tracking decoders like DMR or P25 or set the squelch threshold high enough to avoid changes in noise floor from polyphase channelizer where it can vary between using the one channel or the two-channel combined channel sources.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-07 07:27:44 -04:00
Dennis Sheirer
5654254cbb Revert "#1531 Updates version to 0.6.0 alpha 7 (#1532)"
This reverts commit 11f37e896a.
2023-04-24 04:39:20 -04:00
Denny Sheirer
11f37e896a #1531 Updates version to 0.6.0 alpha 7 (#1532)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-24 04:38:27 -04:00
Denny Sheirer
e8f563deeb #1529 Adds logging suppression to Broadcaster to limit total logged error messages. (#1530)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-24 04:36:02 -04:00
Denny Sheirer
f877354a11 #1455 Add Airspy HF+/Discovery Tuner Support (#1524)
* #1455 Add support for airspy hf+/discovery tuners

* #1455 testing updates

---------

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-15 07:18:37 -04:00
Denny Sheirer
df3d3b5ddc #1519 Adds null checking to channel results array to detect null on tuner shutdown. (#1520)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-13 04:31:46 -04:00
186 changed files with 9210 additions and 2414 deletions

2
.idea/misc.xml generated
View File

@@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_19_PREVIEW" project-jdk-name="liberica-19.0.1.fx" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_19_PREVIEW" project-jdk-name="19" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,17 +23,16 @@ import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Alias Model contains all aliases and is responsible for creation and management of alias lists. Alias lists are a
@@ -184,10 +183,8 @@ public class AliasModel
*/
public void addAliases(List<Alias> aliases)
{
for(Alias alias : aliases)
{
addAlias(alias);
}
removeAliases(aliases);
mAliases.addAll(aliases);
}
/**
@@ -241,6 +238,26 @@ public class AliasModel
}
}
/**
* Removes the list of aliases from this model and any alias lists that might contain each alias.
* @param aliases
*/
public void removeAliases(List<Alias> aliases)
{
if(aliases != null && !aliases.isEmpty())
{
mAliases.removeAll(aliases);
for(Alias alias: aliases)
{
if(hasAliasList(alias.getAliasListName()))
{
getAliasList(alias.getAliasListName()).removeAlias(alias);
}
}
}
}
/**
* Retrieves all aliases that match the alias list and have at least one alias ID of the specified type
* @param aliasListName to search

View File

@@ -34,6 +34,8 @@ import io.github.dsheirer.protocol.Protocol;
*/
public enum TalkgroupFormat
{
AM("*****", 1, 0xFFFF, "1 to 65,535",
"AM valid value range is 1-65,535"),
APCO25("********", 0, 0xFFFF, "0 to 65,535",
"<html>APCO25 talkgroup valid range is 0 to 65,535"),
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
@@ -122,6 +124,8 @@ public enum TalkgroupFormat
switch(protocol)
{
case AM:
return AM;
case APCO25:
return APCO25;
case FLEETSYNC:

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,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.alias.id.talkgroup;
@@ -23,21 +22,20 @@ package io.github.dsheirer.alias.id.talkgroup;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AnalogTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.NBFMTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.EnumMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Protocol-specific formatting of talkgroup values and parsing of formatted talkgroup values.
@@ -49,6 +47,10 @@ public class TalkgroupFormatter
static
{
AnalogTalkgroupFormatter analogTalkgroupFormatter = new AnalogTalkgroupFormatter();
mFormatterMap.put(Protocol.AM, analogTalkgroupFormatter);
mFormatterMap.put(Protocol.NBFM, analogTalkgroupFormatter);
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
mFormatterMap.put(Protocol.FLEETSYNC, new FleetsyncTalkgroupFormatter());
@@ -57,7 +59,6 @@ public class TalkgroupFormatter
mFormatterMap.put(Protocol.LTR_NET, ltr);
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
mFormatterMap.put(Protocol.NBFM, new NBFMTalkgroupFormatter());
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}

View File

@@ -141,11 +141,16 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
@Override
public void receive(SquelchStateEvent event)
{
mSquelchState = event.getSquelchState();
SquelchState squelchState = event.getSquelchState();
if(mSquelchState == SquelchState.SQUELCH)
if(mSquelchState != squelchState)
{
closeAudioSegment();
mSquelchState = squelchState;
if(mSquelchState == SquelchState.SQUELCH)
{
closeAudioSegment();
}
}
}
}

View File

@@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
@@ -46,6 +47,7 @@ import java.net.SocketAddress;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),

View File

@@ -22,6 +22,9 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
@@ -61,6 +64,9 @@ public class BroadcastFactory
case BROADCASTIFY_CALL:
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case RDIOSCANNER_CALL:
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case BROADCASTIFY:
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
inputAudioFormat, mp3Setting, aliasModel);
@@ -99,6 +105,8 @@ public class BroadcastFactory
{
case BROADCASTIFY_CALL:
return new BroadcastifyCallConfiguration(format);
case RDIOSCANNER_CALL:
return new RdioScannerConfiguration(format);
case BROADCASTIFY:
return new BroadcastifyFeedConfiguration(format);
case ICECAST_HTTP:

View File

@@ -31,6 +31,7 @@ public enum BroadcastServerType
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),

View File

@@ -0,0 +1,56 @@
/*
* *****************************************************************************
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.rdioscanner;
/**
* HTTP headers used for posting to Rdio Scanner API
*/
public enum FormField
{
AUDIO("audio"),
AUDIO_NAME("audioName"),
AUDIO_TYPE("audioType"),
DATE_TIME("dateTime"),
FREQUENCIES("frequencies"),
FREQUENCY("frequency"),
KEY("key"),
PATCHES("patches"),
SOURCE("source"),
SOURCES("sources"),
SYSTEM("system"),
SYSTEM_LABEL("systemLabel"),
TALKGROUP_ID("talkgroup"),
TALKGROUP_GROUP("talkgroupGroup"),
TALKGROUP_LABEL("talkgroupLabel"),
TALKGROUP_TAG("talkgroupTag"),
TEST("test");
private String mHeader;
FormField(String header)
{
mHeader = header;
}
public String getHeader()
{
return mHeader;
}
}

View File

@@ -0,0 +1,622 @@
/*
* *****************************************************************************
* 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.audio.broadcast.rdioscanner;
import com.google.common.net.HttpHeaders;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Audio broadcaster to push completed audio recordings to the Rdio Scanner call upload API.
*
*/
public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScannerConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(RdioScannerBroadcaster.class);
private static final String ENCODING_TYPE_MP3 = "mp3";
private static final String MULTIPART_TYPE = "multipart";
private static final String DEFAULT_SUBTYPE = "form-data";
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
private HttpClient mHttpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
* @param config to use
* @param aliasModel for access to aliases
*/
public RdioScannerBroadcaster(RdioScannerConfiguration config, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(config);
mAliasModel = aliasModel;
}
/**
* Starts the audio recording processor thread
*/
@Override
public void start()
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
/**
* Rdio Scanner API does not currently expose a test method.
*/
if(response != null && response.toLowerCase().startsWith("incomplete call data: no talkgroup"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
mLog.error("Error connecting to Rdio Scanner server on startup [" + response + "]");
setBroadcastState(BroadcastState.ERROR);
}
if(mAudioRecordingProcessorFuture == null)
{
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
0, 500, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the audio recording processor thread
*/
@Override
public void stop()
{
if(mAudioRecordingProcessorFuture != null)
{
mAudioRecordingProcessorFuture.cancel(true);
mAudioRecordingProcessorFuture = null;
dispose();
setBroadcastState(BroadcastState.DISCONNECTED);
}
}
/**
* Prepares for disposal
*/
@Override
public void dispose()
{
AudioRecording audioRecording = mAudioRecordingQueue.poll();
while(audioRecording != null)
{
audioRecording.removePendingReplay();
audioRecording = mAudioRecordingQueue.poll();
}
}
/**
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
* the audio processor thread will persistently invoke this method to attempt a reconnect.
*
* Rdio Scanner does not have a test API endpoint, so we look for the incomplete call response.
*/
private boolean connected()
{
if(getBroadcastState() != BroadcastState.CONNECTED &&
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
if(response != null && response.toLowerCase().startsWith("incomplete call data: no talkgroup"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
setBroadcastState(BroadcastState.ERROR);
}
}
return getBroadcastState() == BroadcastState.CONNECTED;
}
@Override
public int getAudioQueueSize()
{
return mAudioRecordingQueue.size();
}
@Override
public void receive(AudioRecording audioRecording)
{
mAudioRecordingQueue.offer(audioRecording);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
/**
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
* deleted to ensure that the in-memory queue size doesn't blow up.
* @param audioRecording to test
* @return true if the recording is valid
*/
private boolean isValid(AudioRecording audioRecording)
{
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
getBroadcastConfiguration().getMaximumRecordingAge();
}
/**
* Processes any enqueued audio recordings. This method employs asynchronous
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
*/
private void processRecordingQueue()
{
while(connected() && !mAudioRecordingQueue.isEmpty())
{
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
{
float durationSeconds = (float)(audioRecording.getRecordingLength() / 1E3f);
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
String talkgroup = getTo(audioRecording);
String radioId = getFrom(audioRecording);
Long frequency = getFrequency(audioRecording);
String patches = getPatches(audioRecording);
String talkgroupLabel = getTalkgroupLabel(audioRecording);
String talkgroupGroup = getTalkgroupGroup(audioRecording);
String systemLabel = getSystemLabel(audioRecording);
try
{
byte[] audioBytes = null;
try
{
audioBytes = Files.readAllBytes(audioRecording.getPath());
}
catch(IOException e)
{
mLog.error("Rdio Scanner API - audio recording file not found - ignoring upload");
}
if(audioBytes != null)
{
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
.addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
.addFile(audioBytes)
.addPart(FormField.DATE_TIME, timestampSeconds)
.addPart(FormField.TALKGROUP_ID, talkgroup)
.addPart(FormField.SOURCE, radioId)
.addPart(FormField.FREQUENCY, frequency)
.addPart(FormField.TALKGROUP_LABEL, talkgroupLabel)
.addPart(FormField.TALKGROUP_GROUP, talkgroupGroup)
.addPart(FormField.SYSTEM_LABEL, systemLabel)
.addPart(FormField.PATCHES, patches);
HttpRequest fileRequest = HttpRequest.newBuilder()
.uri(URI.create(getBroadcastConfiguration().getHost()))
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
.POST(bodyBuilder.build())
.build();
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
.whenComplete((fileResponse, throwable1) -> {
if(throwable1 != null || fileResponse.statusCode() != 200)
{
if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
{
//We get socket reset exceptions occasionally when the remote server doesn't
//fully read our request and immediately responds.
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("Rdio Scanner API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("Rdio Scanner API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
incrementErrorAudioCount();
broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
}
else
{
String fileResponseString = fileResponse.body();
if(fileResponseString.contains("Call imported successfully."))
{
incrementStreamedAudioCount();
broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
else if(fileResponseString.contains("duplicate call rejected"))
{
//Rdio Scanner is telling us to skip audio upload - someone already uploaded it
audioRecording.removePendingReplay();
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("Rdio Scanner API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
}
});
}
else
{
//Register an error for the file not found exception
mLog.error("Rdio Scanner API - upload file not found [" +
audioRecording.getPath().toString() + "]");
incrementErrorAudioCount();
broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
}
catch(Exception e)
{
mLog.error("Unknown Error", e);
setBroadcastState(BroadcastState.ERROR);
incrementErrorAudioCount();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
}
}
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
AudioRecording audioRecording = mAudioRecordingQueue.peek();
while(audioRecording != null)
{
if(isValid(audioRecording))
{
return;
}
else
{
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
mAudioRecordingQueue.poll();
audioRecording.removePendingReplay();
incrementAgedOffAudioCount();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
audioRecording = mAudioRecordingQueue.peek();
}
}
}
/**
* Creates a frequency value from the audio recording identifier collection.
*/
private static Long getFrequency(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
Form.CHANNEL_FREQUENCY, Role.ANY);
if(identifier instanceof ConfigurationLongIdentifier)
{
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
if(value != null)
{
return value;
}
}
return Long.valueOf(0);
}
/**
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
*/
private static String getFrom(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof RadioIdentifier)
{
return ((RadioIdentifier)identifier).getValue().toString();
}
}
return "0";
}
/**
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*
*/
private static String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
}
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
}
return "0";
}
/**
* Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
* If this is a PatchGroup we return only the first label as the primary talkgroup label.
*
*/
private String getTalkgroupLabel(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
StringBuilder sb = new StringBuilder();
if(identifier != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
if(!aliases.isEmpty())
{
sb.append(aliases.get(0));
}
}
return sb.toString();
}
/**
* Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
* If this is a PatchGroup we return only the first group as the primary talkgroup group.
*
*/
private String getTalkgroupGroup(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
StringBuilder sb = new StringBuilder();
if(identifier != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
if(!aliases.isEmpty())
{
sb.append(aliases.get(0).getGroup());
}
}
return sb.toString();
}
/**
* Creates a formatted string with the System Label from the Audio Recording Alias
* If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
*
*/
private String getSystemLabel(AudioRecording audioRecording)
{
List<Identifier> systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
StringBuilder sb = new StringBuilder();
if(!systems.isEmpty())
{
sb.append(systems.get(0));
}
return sb.toString();
}
/**
* Creates a formatted string with the patched talkgroups
*/
public static String getPatches(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
PatchGroup patchGroup = patchGroupIdentifier.getValue();
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
sb.append("]");
return sb.toString();
}
return "[]";
}
/**
* Tests both the connection and configuration against the RdioScanner Call API service
* @param configuration containing API key and system id
* @return error string or null if test is successful
*/
public static String testConnection(RdioScannerConfiguration configuration)
{
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
bodyBuilder.addPart(FormField.KEY, configuration.getApiKey())
.addPart(FormField.SYSTEM, configuration.getSystemID())
.addPart(FormField.TEST, 1);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(configuration.getHost()))
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.header(HttpHeaders.ACCEPT, "*/*")
.POST(bodyBuilder.build())
.build();
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
try
{
HttpResponse<String> response = httpClient.send(request, responseHandler);
String responseBody = response.body();
return (responseBody != null ? responseBody : "(no response)") + " Status Code:" + response.statusCode();
}
catch(Exception e)
{
return e.getLocalizedMessage();
}
}
public class AudioRecordingProcessor implements Runnable
{
@Override
public void run()
{
processRecordingQueue();
}
}
public static void main(String[] args)
{
mLog.debug("Starting ...");
RdioScannerConfiguration config = new RdioScannerConfiguration();
config.setHost("https://api.RdioScanner.com/call-upload-dev");
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
config.setSystemID(11);
String response = testConnection(config);
if(response == null)
{
mLog.debug("Test Successful!");
}
else
{
if(response.contains("1 Invalid-API-Key"))
{
mLog.error("Invalid API Key");
}
else if(response.contains("1 API-Key-Access-Denied"))
{
mLog.error("System ID not valid for API Key");
}
else
{
mLog.debug("Response: " + response);
}
}
mLog.debug("Finished!");
}
}

View File

@@ -0,0 +1,216 @@
/*
* *****************************************************************************
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.rdioscanner;
import java.net.http.HttpRequest;
import java.util.ArrayList;
import java.util.List;
import java.lang.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Builder for an HTTP body publisher to produce a RdioScanner call event
*/
public class RdioScannerBuilder
{
private static final String DASH_DASH = "--";
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
private List<Part> mParts = new ArrayList<>();
private byte[] audioBytes = null;
/**
* Constructs an instance
*/
public RdioScannerBuilder()
{
}
/**
* Access the static multi-part boundary string
*/
public String getBoundary()
{
return BOUNDARY;
}
/**
* Adds a Audio bytes part to the call
*/
public RdioScannerBuilder addFile(byte[] value)
{
audioBytes = value;
return this;
}
/**
* Adds a string part to the call
*/
public RdioScannerBuilder addPart(FormField key, String value)
{
if(key != null && value != null)
{
mParts.add(new Part(key.getHeader(), value));
}
return this;
}
/**
* Adds a number part to the call
*/
public RdioScannerBuilder addPart(FormField key, Number value)
{
if(key != null && value != null)
{
mParts.add(new Part(key.getHeader(), value.toString()));
}
return this;
}
/**
* Creates a form-data item
*/
private static String formatPart(Part part, String boundary)
{
StringBuilder sb = new StringBuilder();
sb.append(DASH_DASH).append(boundary).append("\r\n");
sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
sb.append(part.getValue()).append("\r\n");
return sb.toString();
}
/**
* Creates the audio file item
*/
private String formatFilePart(String boundary)
{
if(audioBytes == null)
{
return "";
}
StringBuilder sb= new StringBuilder();
sb.append(DASH_DASH).append(boundary).append("\r\n");
sb.append("Content-Disposition: form-data; name=\"").append("audio").append("\"\r\n\r\n");
return sb.toString();
}
/**
* Creates the boundary closing item
*/
private static String getClosingBoundary(String boundary)
{
StringBuilder sb = new StringBuilder();
sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
return sb.toString();
}
/**
* Creates a BodyPublisher for accessing the call form data
*/
public HttpRequest.BodyPublisher build()
{
StringBuilder sb = new StringBuilder();
for(Part part: mParts)
{
sb.append(formatPart(part, BOUNDARY));
}
sb.append(formatFilePart(BOUNDARY));
/**
* We need to create a ByteArray consisting of the Sting "parts" and the audio file bytes
*/
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try
{
outputStream.write(sb.toString().getBytes());
if(audioBytes != null)
{
outputStream.write(audioBytes);
}
sb = new StringBuilder();
sb.append("\r\n");
sb.append(getClosingBoundary(BOUNDARY));
outputStream.write(sb.toString().getBytes());
}
catch(IOException e)
{
//mLog.error("Rdio Scanner API - unable to create POST reqeust.");
}
return HttpRequest.BodyPublishers.ofByteArray(outputStream.toByteArray());
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
for(Part part: mParts)
{
sb.append(formatPart(part, BOUNDARY));
}
sb.append(getClosingBoundary(BOUNDARY));
return sb.toString();
}
/**
* Key:Value pair holder
*/
public class Part
{
private String mKey;
private String mValue;
/**
* Constructs a new part
* @param key value
* @param value item
*/
public Part(String key, String value)
{
mKey = key;
mValue = value;
}
public String getKey()
{
return mKey;
}
public String getValue()
{
return mValue;
}
}
}

View File

@@ -0,0 +1,132 @@
/*
* *****************************************************************************
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.rdioscanner;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* Streaming configuration for Rdio Scanner API.
*
* Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
* with the structure of the audio streaming subsystem in sdrtrunk
*/
public class RdioScannerConfiguration extends BroadcastConfiguration
{
private IntegerProperty mSystemID = new SimpleIntegerProperty();
private StringProperty mApiKey = new SimpleStringProperty();
/**
* Constructor for faster jackson
*/
public RdioScannerConfiguration()
{
this(BroadcastFormat.MP3);
}
/**
* Public constructor.
* @param format to use for audio recording (MP3)
*/
public RdioScannerConfiguration(BroadcastFormat format)
{
super(format);
//The parent class binds this property, so we unbind it and rebind it here
mValid.unbind();
mValid.bind(Bindings.and(Bindings.and(Bindings.greaterThan(mSystemID, 0), Bindings.isNotNull(mApiKey)),
Bindings.isNotNull(mHost)));
}
/**
* System ID as a property
*/
public IntegerProperty systemIDProperty()
{
return mSystemID;
}
/**
* API key as a property
*/
public StringProperty apiKeyProperty()
{
return mApiKey;
}
/**
* API Key
*/
@JacksonXmlProperty(isAttribute = true, localName = "api_key")
public String getApiKey()
{
return mApiKey.get();
}
/**
* Sets the api key
* @param apiKey
*/
public void setApiKey(String apiKey)
{
mApiKey.setValue(apiKey);
}
/**
* System ID as provided by RdioScanner.com
*/
@JacksonXmlProperty(isAttribute = true, localName = "system_id")
public int getSystemID()
{
return mSystemID.get();
}
/**
* Sets the system ID provided by RdioScanner.com
*/
public void setSystemID(int systemID)
{
mSystemID.set(systemID);
}
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.RDIOSCANNER_CALL;
}
@Override
public BroadcastConfiguration copyOf()
{
RdioScannerConfiguration copy = new RdioScannerConfiguration();
copy.setSystemID(getSystemID());
return copy;
}
}

View File

@@ -0,0 +1,126 @@
/*
*
* * ******************************************************************************
* * 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/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.broadcast.rdioscanner;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RdioScannerFeedConfiguration extends IcecastTCPConfiguration
{
private final static Logger mLog = LoggerFactory.getLogger(RdioScannerFeedConfiguration.class);
private int mFeedID;
public RdioScannerFeedConfiguration()
{
//No-arg constructor for JAXB
this(BroadcastFormat.MP3);
}
/**
* RdioScanner configuration for Icecast 2.3.2 compatible servers
*
* @param format of output audio (MP3)
*/
public RdioScannerFeedConfiguration(BroadcastFormat format)
{
super(format);
setBitRate(16);
setChannels(1);
setSampleRate(8000);
setInline(true);
}
public static RdioScannerFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
{
RdioScannerFeedConfiguration config = new RdioScannerFeedConfiguration(BroadcastFormat.MP3);
config.setName(userFeedBroadcast.getDescription());
config.setHost(userFeedBroadcast.getHostname());
config.setMountPoint(userFeedBroadcast.getMount());
config.setFeedID(userFeedBroadcast.getFeedId());
config.setPassword(userFeedBroadcast.getPassword());
try
{
config.setPort(Integer.parseInt(userFeedBroadcast.getPort()));
}
catch(Exception e)
{
mLog.error("Error creating Rdio Scanner configuration from radio reference user feed instance");
}
return config;
}
@Override
public BroadcastConfiguration copyOf()
{
RdioScannerFeedConfiguration copy = new RdioScannerFeedConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setInline(getInline());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
//RdioScanner Configuration Parameters
copy.setFeedID(getFeedID());
return copy;
}
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.RDIOSCANNER_CALL;
}
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
public int getFeedID()
{
return mFeedID;
}
public void setFeedID(int feedID)
{
mFeedID = feedID;
}
}

View File

@@ -79,6 +79,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private boolean mRunning = false;
private ScheduledExecutorService mScheduledExecutorService;
private ScheduledFuture<?> mProcessorFuture;
private boolean mDropDuplicates;
private long mOutputLastTimestamp = 0;
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
@@ -104,6 +105,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mixerChannel.name()));
mUserPreferences = userPreferences;
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
try
{
@@ -163,7 +165,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
updateToneInsertionAudioClips();
//Register to receive directory preference update notifications so we can update the preference items
//Register to receive preference update notifications so we can update the preference items
MyEventBus.getGlobalEventBus().register(this);
}
@@ -226,6 +228,10 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
updateToneInsertionAudioClips();
}
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
{
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
}
}
/**
@@ -301,6 +307,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
mCurrentAudioSegment.decrementConsumerCount();
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
mCurrentAudioSegment = null;
broadcast(null);
}
}
@@ -340,13 +347,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
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;
@@ -359,10 +359,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
private boolean isThrowaway(AudioSegment audioSegment)
{
return audioSegment != null &&
(audioSegment.isDoNotMonitor() ||
(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference()
.isDuplicatePlaybackSuppressionEnabled()));
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
}
/**
@@ -378,8 +375,8 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
loadNextAudioSegment();
}
//Reevaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
if(isThrowaway(mCurrentAudioSegment))
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
while(isThrowaway(mCurrentAudioSegment))
{
if(mCurrentBufferIndex > 0)
{
@@ -392,24 +389,42 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
while(mCurrentAudioSegment != null && mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
{
try
//Continuously evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
if(isThrowaway(mCurrentAudioSegment))
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
if(audioBuffer != null)
if(mCurrentBufferIndex > 0)
{
ByteBuffer audio = convert(audioBuffer);
//This call blocks until all audio bytes are dumped into the data line.
playAudio(audio);
playAudio(mAudioSegmentDropTone);
}
disposeCurrentAudioSegment();
}
catch(Exception e)
else
{
mLog.error("Error while processing audio for [" + mMixerChannel.name() + "]", e);
if(mCurrentBufferIndex == 0)
{
playAudio(mAudioSegmentStartTone);
}
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
//Check for completed and fully-played audio segment to closeout
if(mCurrentAudioSegment != null &&
mCurrentAudioSegment.isComplete() &&
(mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
@@ -651,16 +666,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
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)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -84,7 +84,7 @@ public class DcCorrectionManager
{
mDcCalculationsRemaining--;
}
//
// mLog.info("DC: " + mDecimalFormat.format(mAverageDc) +
// " RES:" + mDecimalFormat.format(residualOffsetToRemove) +
// " REM:" + mDcCalculationsRemaining);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,21 +21,16 @@ package io.github.dsheirer.buffer;
import java.util.Arrays;
/**
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples. Initially
* fills buffer with 0-valued samples
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
* Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with
* newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value.
*/
public class FloatCircularBuffer
{
private float[] mBuffer;
private int mBufferPointer = 0;
private float mOldestSample;
/**
* Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
*
* @param size of the buffer
* @param initialFillValue to fill the buffer entries
*/
@@ -47,7 +42,6 @@ public class FloatCircularBuffer
/**
* Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
*
* @param size of the buffer
*/
public FloatCircularBuffer(int size)
@@ -55,33 +49,34 @@ public class FloatCircularBuffer
this(size, 0.0f);
}
public float[] getBuffer()
/**
* Resets the delay buffer to an all zeros state
*/
public void reset(float fillValue)
{
return mBuffer;
Arrays.fill(mBuffer, fillValue);
}
/**
* Gets the current buffer value and overwrites that value position in the buffer with the new value.
* Puts the new value into the buffer and returns the oldest buffer value that it replaced
*
* @param newValue to add to the buffer
* @return oldest value that was overwritten by the new value
*/
public float getAndPut(float newValue)
public float get(float newValue)
{
mOldestSample = mBuffer[mBufferPointer];
put(newValue);
return mOldestSample;
}
float oldestSample = mBuffer[mBufferPointer];
/**
* Puts the value into the buffer, updates the pointer and returns the buffer value at the pointer position.
* @param sample to add
* @return next pointed sample.
*/
public float putAndGet(float sample)
{
put(sample);
return mBuffer[mBufferPointer];
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
return oldestSample;
}
public float[] getAll()
@@ -105,13 +100,70 @@ public class FloatCircularBuffer
/**
* Places the new value into the buffer, overwriting the oldest value
*
* @param newValue to add to the buffer
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
mBufferPointer %= mBuffer.length;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the maximum from the values currently in the buffer
*/
public float max(float referenceValue)
{
float max = referenceValue;
for(int x = 0; x < mBuffer.length; x++)
{
if(mBuffer[x] > max)
{
max = mBuffer[x];
}
}
// System.out.println("Max [" + max + "] from " + Arrays.toString(mBuffer));
return max;
}
/**
* Returns the minimum from the values currently in the buffer
*/
public float min()
{
float min = 0.0f;
for(int x = 0; x < mBuffer.length; x++)
{
if(mBuffer[x] < min)
{
min = mBuffer[x];
}
}
return min;
}
/**
* Calculates the average/mean of the values contained in this buffer
*/
public float mean()
{
double accumulator = 0.0;
for(float value: mBuffer)
{
accumulator += value;
}
return (float)(accumulator / mBuffer.length);
}
}

View File

@@ -1,124 +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.buffer;
/**
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples.
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/
public class RealCircularBuffer
{
float[] mBuffer;
int mBufferPointer = 0;
public RealCircularBuffer(int size)
{
mBuffer = new float[size];
}
public int getSize()
{
return mBuffer.length;
}
public float[] getBuffer()
{
return mBuffer;
}
/**
* Puts the new value into the buffer and returns the oldest buffer value
* that it replaced
*
* @param newValue
* @return
*/
public float putAndGet(float newValue)
{
float oldestSample = mBuffer[mBufferPointer];
put(newValue);
return oldestSample;
}
/**
* Adds the new value to this buffer.
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the
* newest sample.
* @param index in range of 0 to length-1
* @return value at indexed position
*/
public float get(int index)
{
if(index < mBuffer.length)
{
int pointer = mBufferPointer + index;
if(pointer >= mBuffer.length)
{
pointer -= mBuffer.length;
}
return mBuffer[pointer];
}
else
{
throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" +
mBuffer.length + "]");
}
}
/**
* Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1
* is the newest sample.
*
* @return array of samples
*/
public float[] get()
{
float[] samples = new float[mBuffer.length];
System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer);
if(mBufferPointer != 0)
{
System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer);
}
return samples;
}
}

View File

@@ -0,0 +1,132 @@
/*
* *****************************************************************************
* 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.buffer.airspy.hf;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Native buffer implementation for Airspy HF+ & Discovery tuners.
*/
public class AirspyHfNativeBuffer extends AbstractNativeBuffer
{
public static final float SCALE = 1.0f / 32768.0f;
private short[] mInterleavedSamples;
private float mAverageDc;
/**
* Constructs an instance.
* @param timestamp of the first sample of the buffer
* @param interleavedSamples pairs of 16-bit complex samples.
*/
public AirspyHfNativeBuffer(long timestamp, float samplesPerMillisecond, float averageDc, short[] interleavedSamples)
{
super(timestamp, samplesPerMillisecond);
mInterleavedSamples = interleavedSamples;
mAverageDc = averageDc;
}
@Override
public Iterator<ComplexSamples> iterator()
{
return new IteratorComplexSamples();
}
@Override
public Iterator<InterleavedComplexSamples> iteratorInterleaved()
{
return new IteratorInterleaved();
}
@Override
public int sampleCount()
{
return mInterleavedSamples.length / 2;
}
/**
* Scalar implementation of complex samples buffer iterator
*/
public class IteratorComplexSamples implements Iterator<ComplexSamples>
{
private boolean mHasNext = true;
@Override
public boolean hasNext()
{
return mHasNext;
}
@Override
public ComplexSamples next()
{
int length = mInterleavedSamples.length / 2;
float[] i = new float[length];
float[] q = new float[length];
int index = 0;
for(int x = 0; x < mInterleavedSamples.length; x += 2)
{
index = x / 2;
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
i[index] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
q[index] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new ComplexSamples(i, q, getTimestamp());
}
}
/**
* Scalar implementation of interleaved sample buffer iterator.
*/
public class IteratorInterleaved implements Iterator<InterleavedComplexSamples>
{
private boolean mHasNext = true;
@Override
public boolean hasNext()
{
return mHasNext;
}
@Override
public InterleavedComplexSamples next()
{
float[] samples = new float[mInterleavedSamples.length];
for(int x = 0; x < mInterleavedSamples.length; x += 2)
{
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
samples[x] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
samples[x + 1] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new InterleavedComplexSamples(samples, getTimestamp());
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.buffer.airspy.hf;
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.DcCorrectionManager;
import io.github.dsheirer.buffer.INativeBuffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
/**
* Airspy HF+ native buffer factory.
*
* Note: the tuner provides 1024 samples in each transfer where samples are 16-bit interleaved complex.
*/
public class AirspyHfNativeBufferFactory extends AbstractNativeBufferFactory
{
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();
/**
* Constructs an instance
*/
public AirspyHfNativeBufferFactory()
{
}
/**
* Converts the samples byte buffer into a native buffer and calculates DC offset
* @param samples byte array copied from native memory
* @param timestamp of the samples
* @return constructed native buffer
*/
@Override
public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
{
ShortBuffer shortBuffer = samples.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short[] converted = new short[shortBuffer.capacity()];
shortBuffer.get(converted);
if(mDcCorrectionManager.shouldCalculateDc())
{
calculateDc(converted);
}
return new AirspyHfNativeBuffer(timestamp, getSamplesPerMillisecond(), mDcCorrectionManager.getAverageDc(), converted);
}
/**
* Calculates the average DC in the sample stream so that it can be subtracted from the samples when the
* native buffer is used.
* @param samples containing DC offset
*/
private void calculateDc(short[] samples)
{
float dcAccumulator = 0;
for(short sample: samples)
{
dcAccumulator += sample;
}
dcAccumulator /= samples.length;
mDcCorrectionManager.adjust(dcAccumulator * AirspyHfNativeBuffer.SCALE);
}
}

View File

@@ -73,7 +73,7 @@ public class DecoderStateEvent
{
StringBuilder sb = new StringBuilder();
sb.append("Decoder State Event - source[").append(mSource.getClass())
sb.append("Decoder State Event - source[").append(mSource != null ? mSource.getClass() : "null")
.append("] event[").append(mEvent)
.append("] state[").append(mState)
.append("] timeslot[").append(mTimeslot)

View File

@@ -0,0 +1,286 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.calibrate.Calibration;
import io.github.dsheirer.vector.calibrate.CalibrationException;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
/**
* Calibrates AM demodulator options
*/
public class AmDemodulatorCalibration extends Calibration
{
private static final int BUFFER_SIZE = 2048;
private static final int ITERATION_DURATION_MS = 1000;
private static final int WARMUP_ITERATIONS = 5;
private static final int TEST_ITERATIONS = 5;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
private IAmDemodulator mScalarDemodulator = new ScalarAMDemodulator(500.0f);
private IAmDemodulator mVectorDemodulator64 = new VectorAMDemodulator64(500.0f);
private IAmDemodulator mVectorDemodulator128 = new VectorAMDemodulator128(500.0f);
private IAmDemodulator mVectorAMDemodulator256 = new VectorAMDemodulator256(500.0f);
private IAmDemodulator mVectorAMDemodulator512 = new VectorAMDemodulator512(500.0f);
/**
* Constructs an instance
*/
public AmDemodulatorCalibration()
{
super(CalibrationType.AM_DEMODULATOR);
}
@Override public void calibrate() throws CalibrationException
{
float[] magnitude = getFloatSamples(BUFFER_SIZE);
Mean scalarMean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testScalar(magnitude);
scalarMean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult()));
Mean vector64Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector64(magnitude);
vector64Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 64: " + DECIMAL_FORMAT.format(vector64Mean.getResult()));
Mean vector128Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector128(magnitude);
vector128Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 128: " + DECIMAL_FORMAT.format(vector128Mean.getResult()));
Mean vector256Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector256(magnitude);
vector256Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 256: " + DECIMAL_FORMAT.format(vector256Mean.getResult()));
Mean vector512Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector512(magnitude);
vector512Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 512: " + DECIMAL_FORMAT.format(vector512Mean.getResult()));
//Start tests
scalarMean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testScalar(magnitude);
scalarMean.increment(score);
}
mLog.info("AM DEMODULATOR - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult()));
vector64Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector64(magnitude);
vector64Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 64: " + DECIMAL_FORMAT.format(vector64Mean.getResult()));
vector128Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector128(magnitude);
vector128Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 128: " + DECIMAL_FORMAT.format(vector128Mean.getResult()));
vector256Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector256(magnitude);
vector256Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 256: " + DECIMAL_FORMAT.format(vector256Mean.getResult()));
vector512Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector512(magnitude);
vector512Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 512: " + DECIMAL_FORMAT.format(vector512Mean.getResult()));
Implementation optimal = Implementation.SCALAR;
double optimalResult = scalarMean.getResult();
if(vector64Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_64;
optimalResult = vector64Mean.getResult();
}
if(vector128Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_128;
optimalResult = vector128Mean.getResult();
}
if(vector256Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_256;
optimalResult = vector256Mean.getResult();
}
if(vector512Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_512;
}
setImplementation(optimal);
mLog.info("AM DEMODULATOR - SET OPTIMAL IMPLEMENTATION TO:" + getImplementation());
}
private long testScalar(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mScalarDemodulator.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector64(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorDemodulator64.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector128(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorDemodulator128.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector256(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorAMDemodulator256.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector512(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorAMDemodulator512.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
public static void main(String[] args)
{
AmDemodulatorCalibration calibration = new AmDemodulatorCalibration();
try
{
calibration.calibrate();
}
catch(Exception e)
{
mLog.error("Error during calibration", e);
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.am;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for creating AM demodulators
*/
public class AmDemodulatorFactory
{
private static final Logger mLog = LoggerFactory.getLogger(AmDemodulatorFactory.class);
/**
* Creates the optimal AM demodulator using calibration data to select the optimal
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static IAmDemodulator getAmDemodulator(float gain)
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.AM_DEMODULATOR);
switch(implementation)
{
case VECTOR_SIMD_64:
return new VectorAMDemodulator64(gain);
case VECTOR_SIMD_128:
return new VectorAMDemodulator128(gain);
case VECTOR_SIMD_256:
return new VectorAMDemodulator256(gain);
case VECTOR_SIMD_512:
return new VectorAMDemodulator512(gain);
case VECTOR_SIMD_PREFERRED:
return new VectorAMDemodulator64(gain);
case SCALAR:
case UNCALIBRATED:
return new ScalarAMDemodulator(gain);
default:
mLog.warn("Unrecognized optimal operation for AM demodulator: " + implementation.name());
return new ScalarAMDemodulator(gain);
}
}
}

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* 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.dsp.am;
/**
@@ -6,10 +25,9 @@ package io.github.dsheirer.dsp.am;
public interface IAmDemodulator
{
/**
* Demodulate the complex I & Q sample arrays
* @param i samples
* @param q samples
* @return demodulated samples.
* Demodulates an array of vector squared magnitudes.
* @param magnitude values that are squared.
* @return demodulated samples
*/
float[] demodulate(float[] i, float[] q);
float[] demodulateMagnitude(float[] magnitude);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,7 @@ import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class AMDemodulator implements IAmDemodulator
public class ScalarAMDemodulator implements IAmDemodulator
{
private float mGain;
@@ -32,7 +32,7 @@ public class AMDemodulator implements IAmDemodulator
*
* @param gain to apply to demodulated output samples.
*/
public AMDemodulator(float gain)
public ScalarAMDemodulator(float gain)
{
mGain = gain;
}
@@ -57,13 +57,13 @@ public class AMDemodulator implements IAmDemodulator
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override public float[] demodulate(float[] i, float[] q)
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
float[] demodulated = new float[i.length];
for(int x = 0; x < i.length; x++)
float[] demodulated = new float[magnitude.length];
for(int x = 0; x < magnitude.length; x++)
{
demodulated[x] = (float)FastMath.sqrt((i[x] * i[x]) + (q[x] * q[x])) * mGain;
demodulated[x] = (float)FastMath.sqrt(magnitude[x]) * mGain;
}
return demodulated;

View File

@@ -0,0 +1,161 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.fm.ISquelchingDemodulator;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.dsp.squelch.AdaptiveSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* AM demodulator with integrated squelch control.
*/
public class SquelchingAMDemodulator implements ISquelchingDemodulator, Listener<SourceEvent>
{
private static final float ZERO = 0.0f;
private final IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
private final IAmDemodulator mAmDemodulator;
private final AdaptiveSquelch mAdaptiveSquelch;
private boolean mSquelchChanged = false;
/**
* Constructs an instance
* @param gain to apply to the demodulated AM samples (e.g. 500.0f)
* @param alpha decay value of the single pole IIR filter in range: 0.0 - 1.0. The smaller the alpha value,
* the slower the squelch response.
* @param squelchThreshold in decibels. Signal power must exceed this threshold value for unsquelch.
* @param squelchAutoTrack to enable the squelch noise floor auto tracking feature.
*/
public SquelchingAMDemodulator(float gain, float alpha, float squelchThreshold, boolean squelchAutoTrack)
{
mAmDemodulator = AmDemodulatorFactory.getAmDemodulator(gain);
mAdaptiveSquelch = new AdaptiveSquelch(alpha, squelchThreshold, squelchAutoTrack);
}
/**
* Set or update the sample rate for the squelch to adjust the power level notification rate.
* @param sampleRate in hertz
*/
public void setSampleRate(int sampleRate)
{
mAdaptiveSquelch.setSampleRate(sampleRate);
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mAdaptiveSquelch.setSourceEventListener(listener);
}
/**
* Demodulates the complex sample arrays.
* @param i inphase sample array
* @param q quadrature sample array
* @return demodulated AM samples with gain applied.
*/
public float[] demodulate(float[] i, float[] q)
{
mAdaptiveSquelch.setSquelchChanged(false);
setSquelchChanged(false);
float[] magnitude = mMagnitudeCalculator.calculate(i, q);
float[] demodulated = mAmDemodulator.demodulateMagnitude(magnitude);
for(int x = 0; x < i.length; x++)
{
mAdaptiveSquelch.process(magnitude[x]);
if(!(mAdaptiveSquelch.isUnmuted()))
{
demodulated[x] = ZERO;
}
if(mAdaptiveSquelch.isSquelchChanged())
{
setSquelchChanged(true);
}
}
return demodulated;
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(float threshold)
{
mAdaptiveSquelch.setSquelchThreshold(threshold);
}
@Override
public void setSquelchAutoTrack(boolean autoTrack)
{
mAdaptiveSquelch.setSquelchAutoTrack(autoTrack);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mAdaptiveSquelch.isMuted();
}
/**
* Process source events initiated by the timer and end-user.
* @param sourceEvent to process.
*/
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
//Only forward squelch threshold & auto-track request events
case REQUEST_CURRENT_SQUELCH_THRESHOLD:
case REQUEST_CHANGE_SQUELCH_THRESHOLD:
case REQUEST_CURRENT_SQUELCH_AUTO_TRACK:
case REQUEST_CHANGE_SQUELCH_AUTO_TRACK:
{
mAdaptiveSquelch.receive(sourceEvent);
}
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator128 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator128(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator256 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator256(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator512 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator512(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator64 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator64(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

View File

@@ -421,9 +421,12 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
for(float[] channelResults: list)
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
processedChannelResults.add(channelResults);
if(channelResults != null)
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
processedChannelResults.add(channelResults);
}
}
dispatch(processedChannelResults);

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* 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.dsp.filter.iir;
/**
@@ -40,7 +59,7 @@ public class SinglePoleIirFilter
* @param sample to process/filter
* @return filtered output
*/
public double filter(float sample)
public float filter(float sample)
{
mOutput = (mOutput * mOneMinusAlpha) + (mAlpha * sample);
return mOutput;
@@ -49,7 +68,7 @@ public class SinglePoleIirFilter
/**
* Current output value
*/
public double getValue()
public float getValue()
{
return mOutput;
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -37,40 +37,23 @@ public class FmDemodulatorFactory
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static IFmDemodulator getFmDemodulator()
public static IDemodulator getFmDemodulator()
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.FM_DEMODULATOR);
switch(implementation)
{
case VECTOR_SIMD_PREFERRED:
return new VectorFMDemodulator();
case VECTOR_SIMD_64:
return new VectorFMDemodulator64();
case VECTOR_SIMD_128:
return new VectorFMDemodulator128();
case VECTOR_SIMD_256:
return new VectorFMDemodulator256();
case VECTOR_SIMD_512:
return new VectorFMDemodulator512();
case SCALAR:
return new ScalarFMDemodulator();
default:
mLog.warn("Unrecognized optimal operation for FM demodulator: " + implementation.name());
return new ScalarFMDemodulator();
}
}
/**
* Creates the optimal Squelching FM demodulator using calibration data to select the optimal
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static ISquelchingFmDemodulator getSquelchingFmDemodulator(float alpha, float threshold, int ramp)
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.FM_DEMODULATOR);
switch(implementation)
{
case VECTOR_SIMD_PREFERRED:
return new VectorSquelchingFMDemodulator(alpha, threshold, ramp);
case SCALAR:
return new ScalarSquelchingFMDemodulator(alpha, threshold, ramp);
default:
mLog.warn("Unrecognized optimal operation for Squelching FM demodulator: " + implementation.name());
return new ScalarSquelchingFMDemodulator(alpha, threshold, ramp);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* *****************************************************************************
* 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.dsp.fm;
/**
* Interface for demodulator of complex sample arrays.
*/
public interface IDemodulator
{
/**
* Demodulate the complex sample array and return an array of real audio samples.
* @param i samples to demodulate
* @param q samples to demodulate
* @return demodulated samples.
*/
float[] demodulate(float[] i, float[] q);
}

View File

@@ -1,14 +0,0 @@
package io.github.dsheirer.dsp.fm;
/**
* Interface for FM demodulator
*/
public interface IFmDemodulator
{
/**
* Demodulate the complex sample array and return an array of real audio samples.
* @param samples to demodulate
* @return demodulated samples.
*/
float[] demodulate(float[] i, float[] q);
}

View File

@@ -0,0 +1,69 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* Interface for FM demodulator that provides squelch control.
*/
public interface ISquelchingDemodulator extends IDemodulator
{
/**
* Sets the sample rate so that the squelch can adjust the channel power level broadcast notification rate
* @param sampleRate in hertz
*/
void setSampleRate(int sampleRate);
/**
* Indicates if the squelch state has changed.
*/
boolean isSquelchChanged();
/**
* Indicates if the squelch state is currently muted
*/
boolean isMuted();
/**
* Sets the threshold to use for power squelch
* @param threshold in decibels
*/
void setSquelchThreshold(float threshold);
/**
* Enables or disables the squelch auto-tracking feature where the squelch threshold is auto-adjusted based on the
* observed channel power when the signal is not present (ie squelch state).
* @param autoTrack true to enable
*/
void setSquelchAutoTrack(boolean autoTrack);
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
void setSourceEventListener(Listener<SourceEvent> listener);
/**
* Receives a source event
*/
void receive(SourceEvent sourceEvent);
}

View File

@@ -1,42 +0,0 @@
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* Interface for FM demodulator that provides squelch control.
*/
public interface ISquelchingFmDemodulator extends IFmDemodulator
{
/**
* Indicates if the squelch state has changed.
*/
boolean isSquelchChanged();
/**
* Indicates if the squelch state is currently muted
*/
boolean isMuted();
/**
* Sets the threshold to use for power squelch
* @param threshold in decibels
*/
void setSquelchThreshold(double threshold);
/**
* Resets the demodulator
*/
void reset();
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
void setSourceEventListener(Listener<SourceEvent> listener);
/**
* Receives a source event
*/
void receive(SourceEvent sourceEvent);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,16 +18,15 @@
*/
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*/
public class ScalarFMDemodulator implements IFmDemodulator
public class ScalarFMDemodulator implements IDemodulator
{
private float mPreviousI = 0.0f;
private float mPreviousQ = 0.0f;
protected float mPreviousI = 0.0f;
protected float mPreviousQ = 0.0f;
protected float mGain;
/**
@@ -47,50 +46,6 @@ public class ScalarFMDemodulator implements IFmDemodulator
mGain = gain;
}
/**
* Demodulates the I/Q sample via complex sample multiplication by the complex conjugates of the most recently
* demodulated complex sample (ie previous sample). Each new sample overwrites the previously stored sample to
* allow future invocations of the method to simply use a new sample value.
*
* @param currentI of the sample
* @param currentQ of the sample
* @return demodulated sample
*/
public float demodulate(float currentI, float currentQ)
{
/**
* Multiply the current sample against the complex conjugate of the
* previous sample to derive the phase delta between the two samples
*
* Negating the previous sample quadrature produces the conjugate
*/
double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ);
double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ);
double angle = 0.0f;
//Check for divide by zero
if(inphase != 0)
{
/**
* Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal
*/
double denominator = 1.0d / inphase;
angle = FastMath.atan((double)quadrature * denominator);
}
/**
* Store the current sample to use during the next iteration
*/
mPreviousI = currentI;
mPreviousQ = currentQ;
return (float)(angle * mGain);
}
public float[] demodulate(float[] i, float[] q)
{
float[] demodulated = new float[i.length];
@@ -135,35 +90,6 @@ public class ScalarFMDemodulator implements IFmDemodulator
return demodulated;
}
/**
* Demodulates the complex samples and returns the demodulated value.
* @param previous
* @param current
* @return
*/
public static double demodulate(Complex previous, Complex current)
{
double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature());
double quadrature = (current.quadrature() * previous.inphase()) + (current.inphase() * -previous.quadrature());
double angle = 0.0f;
//Check for divide by zero
if(inphase != 0)
{
/**
* Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal
*/
double denominator = 1.0d / inphase;
angle = FastMath.atan((double)quadrature * denominator);
}
return angle;
}
public void dispose()
{
//no-op

View File

@@ -1,98 +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.dsp.fm;
import io.github.dsheirer.dsp.squelch.PowerSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*
* Implements listener of source events to process runtime squelch threshold change request events
* which are forwarded to the power squelch control.
*/
public class ScalarSquelchingFMDemodulator extends ScalarFMDemodulator implements ISquelchingFmDemodulator, Listener<SourceEvent>
{
private PowerSquelch mPowerSquelch;
private boolean mSquelchChanged = false;
/**
* Creates an FM demodulator instance with a default gain of 1.0.
*/
public ScalarSquelchingFMDemodulator(float alpha, float threshold, int ramp)
{
mPowerSquelch = new PowerSquelch(alpha, threshold, ramp);
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mPowerSquelch.setSourceEventListener(listener);
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(double threshold)
{
mPowerSquelch.setSquelchThreshold(threshold);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mPowerSquelch.isMuted();
}
@Override
public void receive(SourceEvent sourceEvent)
{
//Only forward squelch threshold change request events
if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
}
}

View File

@@ -0,0 +1,161 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.dsp.squelch.AdaptiveSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*
* Implements listener of source events to process runtime squelch threshold change request events
* which are forwarded to the power squelch control.
*/
public class SquelchingFMDemodulator implements ISquelchingDemodulator, Listener<SourceEvent>
{
private static final Logger mLog = LoggerFactory.getLogger(SquelchingFMDemodulator.class);
private static final float ZERO = 0.0f;
private final AdaptiveSquelch mAdaptiveSquelch;
private final IMagnitudeCalculator mMagnitude = MagnitudeFactory.getMagnitudeCalculator();
private final IDemodulator mFmDemodulator = FmDemodulatorFactory.getFmDemodulator();
private boolean mSquelchChanged = false;
/**
* Creates an FM demodulator instance with a default gain of 1.0.
*
* @param alpha decay value of the single pole IIR filter in range: 0.0 - 1.0. The smaller the alpha value,
* the slower the squelch response.
* @param squelchThreshold in decibels. Signal power must exceed this threshold value for unsquelch.
* causes immediate mute and unmute. Set to higher count to prevent mute/unmute flapping.
* @param squelchAutoTrack to enable the squelch noise floor auto-tracking feature.
*/
public SquelchingFMDemodulator(float alpha, float squelchThreshold, boolean squelchAutoTrack)
{
mAdaptiveSquelch = new AdaptiveSquelch(alpha, squelchThreshold, squelchAutoTrack);
}
/**
* Set or update the sample rate for the squelch to adjust the power level notification rate.
* @param sampleRate in hertz
*/
public void setSampleRate(int sampleRate)
{
mAdaptiveSquelch.setSampleRate(sampleRate);
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mAdaptiveSquelch.setSourceEventListener(listener);
}
/**
* Demodulates the complex (I/Q) sample arrays
* @param i inphase samples
* @param q quadrature samples
* @return demodulated real samples
*/
@Override
public float[] demodulate(float[] i, float[] q)
{
setSquelchChanged(false);
float[] demodulated = mFmDemodulator.demodulate(i, q);
float[] magnitude = mMagnitude.calculate(i, q);
for(int x = 0; x < magnitude.length; x++)
{
mAdaptiveSquelch.process(magnitude[x]);
if(!(mAdaptiveSquelch.isUnmuted()))
{
demodulated[x] = ZERO;
}
if(mAdaptiveSquelch.isSquelchChanged())
{
setSquelchChanged(true);
}
}
return demodulated;
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(float threshold)
{
mAdaptiveSquelch.setSquelchThreshold(threshold);
}
@Override
public void setSquelchAutoTrack(boolean autoTrack)
{
mAdaptiveSquelch.setSquelchAutoTrack(autoTrack);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mAdaptiveSquelch.isMuted();
}
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
//Only forward squelch threshold and auto-track current & change requests
case REQUEST_CURRENT_SQUELCH_THRESHOLD:
case REQUEST_CHANGE_SQUELCH_THRESHOLD:
case REQUEST_CURRENT_SQUELCH_AUTO_TRACK:
case REQUEST_CHANGE_SQUELCH_AUTO_TRACK:
mAdaptiveSquelch.receive(sourceEvent);
break;
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,17 +26,12 @@ import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Random;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator implements IFmDemodulator
public abstract class VectorFMDemodulator implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator.class);
protected static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_PREFERRED;
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
@@ -48,9 +43,16 @@ public class VectorFMDemodulator implements IFmDemodulator
{
}
/**
* Vector species as specified by the implementing sub-class.
*/
protected abstract VectorSpecies<Float> getVectorSpecies();
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
// VectorSpecies<Float> species= getVectorSpecies();
VectorUtilities.checkComplexArrayLength(i, q, getVectorSpecies());
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
@@ -70,12 +72,12 @@ public class VectorFMDemodulator implements IFmDemodulator
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += getVectorSpecies().length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
previousI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
@@ -89,53 +91,4 @@ public class VectorFMDemodulator implements IFmDemodulator
return demodulated;
}
public static void main(String[] args)
{
int iterations = 3_000_000;
int sampleSize = 2048;
Random random = new Random();
float[] iSamples = new float[sampleSize];
float[] qSamples = new float[sampleSize];
for(int x = 0; x < iSamples.length; x++)
{
iSamples[x] = random.nextFloat() * 2.0f - 1.0f;
qSamples[x] = random.nextFloat() * 2.0f - 1.0f;
}
boolean validation = false;
ScalarFMDemodulator legacy = new ScalarFMDemodulator();
VectorFMDemodulator vector = new VectorFMDemodulator();
if(validation)
{
float[] legacySamples = legacy.demodulate(iSamples, qSamples);
float[] vectorSamples = vector.demodulate(iSamples, qSamples);
System.out.println("LEGACY:" + Arrays.toString(legacySamples));
System.out.println("VECTOR:" + Arrays.toString(vectorSamples));
}
else
{
System.out.println("Test Starting ...");
long start = System.currentTimeMillis();
double accumulator = 0.0;
for(int i = 0; i < iterations; i++)
{
float[] samples = legacy.demodulate(iSamples, qSamples);
// float[] samples = vector.demodulate(iSamples, qSamples);
accumulator += samples[3];
}
double elapsed = System.currentTimeMillis() - start;
DecimalFormat df = new DecimalFormat("0.000");
System.out.println("Accumulator: " + accumulator);
System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds");
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator128 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator128.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator128()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator256 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator256.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator256()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator512 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator512.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator512()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator64 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator64.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator64()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -1,229 +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.dsp.fm;
import io.github.dsheirer.dsp.squelch.PowerSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Random;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*
* Implements listener of source events to process runtime squelch threshold change request events
* which are forwarded to the power squelch control.
*/
public class VectorSquelchingFMDemodulator extends VectorFMDemodulator
implements ISquelchingFmDemodulator, Listener<SourceEvent>
{
private static final Logger mLog = LoggerFactory.getLogger(VectorSquelchingFMDemodulator.class);
private static final float TWO = 2.0f;
private PowerSquelch mPowerSquelch;
private boolean mSquelchChanged = false;
/**
* Creates an FM demodulator instance with a default gain of 1.0.
*/
public VectorSquelchingFMDemodulator(float alpha, float threshold, int ramp)
{
mPowerSquelch = new PowerSquelch(alpha, threshold, ramp);
}
public void reset()
{
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mPowerSquelch.setSourceEventListener(listener);
}
/**
* Demodulates the complex (I/Q) sample arrays
* @param i inphase samples
* @param q quadrature samples
* @return demodulated real samples
*/
@Override
public float[] demodulate(float[] i, float[] q)
{
setSquelchChanged(false);
float[] demodulated = super.demodulate(i, q);
float[] magnitude = getMagnitude(i, q);
for(int x = 0; x < i.length; x++)
{
mPowerSquelch.process(magnitude[x]);
if(!(mPowerSquelch.isUnmuted() || mPowerSquelch.isDecay()))
{
demodulated[x] = ZERO;
}
if(mPowerSquelch.isSquelchChanged())
{
setSquelchChanged(true);
}
}
return demodulated;
}
/**
* Calculates the magnitude of the complex I/Q sample arrays using SIMD vector intrinsics.
* @param i sample array
* @param q sample array
* @return magnitude array
*/
public static float[] getMagnitude(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
float[] magnitudes = new float[i.length];
FloatVector iVector, qVector;
for(int samplePointer = 0; samplePointer < i.length; samplePointer += VECTOR_SPECIES.length())
{
iVector = FloatVector.fromArray(VECTOR_SPECIES, i, samplePointer);
qVector = FloatVector.fromArray(VECTOR_SPECIES, q, samplePointer);
iVector.pow(TWO).add(qVector.pow(TWO)).intoArray(magnitudes, samplePointer);
}
return magnitudes;
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(double threshold)
{
mPowerSquelch.setSquelchThreshold(threshold);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mPowerSquelch.isMuted();
}
@Override
public void receive(SourceEvent sourceEvent)
{
//Only forward squelch threshold change request events
if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
}
public static void main(String[] args)
{
int iterations = 3_000_000;
int sampleSize = 2048;
Random random = new Random();
float[] iSamples = new float[sampleSize];
float[] qSamples = new float[sampleSize];
for(int x = 0; x < iSamples.length; x++)
{
iSamples[x] = random.nextFloat() * 2.0f - 1.0f;
qSamples[x] = random.nextFloat() * 2.0f - 1.0f;
}
float POWER_SQUELCH_ALPHA_DECAY = 0.0004f;
float POWER_SQUELCH_THRESHOLD_DB = -78.0f;
int POWER_SQUELCH_RAMP = 4;
ScalarFMDemodulator fm = new ScalarFMDemodulator();
VectorFMDemodulator vectorFM = new VectorFMDemodulator();
ScalarSquelchingFMDemodulator sqLegacy = new ScalarSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
VectorSquelchingFMDemodulator vectorSq = new VectorSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
boolean validation = false;
if(validation)
{
float[] noSqFmSamples = fm.demodulate(iSamples, qSamples);
float[] vectorNoSqFmSamples = vectorFM.demodulate(iSamples, qSamples);
float[] sqFmSamples = sqLegacy.demodulate(iSamples, qSamples);
float[] vectorSqFmSamples = vectorSq.demodulate(iSamples, qSamples);
System.out.println(" NO SQ FM:" + Arrays.toString(noSqFmSamples));
System.out.println("VECTOR NO SQ FM:" + Arrays.toString(vectorNoSqFmSamples));
System.out.println(" SQ FM:" + Arrays.toString(sqFmSamples));
System.out.println(" VECTOR SQ FM:" + Arrays.toString(vectorSqFmSamples));
}
else
{
System.out.println("Test Starting ...");
long start = System.currentTimeMillis();
double accumulator = 0.0;
for(int i = 0; i < iterations; i++)
{
// float[] samples = sqLegacy.demodulate(iSamples, qSamples);
float[] samples = vectorSq.demodulate(iSamples, qSamples);
accumulator += samples[3];
}
double elapsed = System.currentTimeMillis() - start;
DecimalFormat df = new DecimalFormat("0.000");
System.out.println("Accumulator: " + accumulator);
System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds");
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* *****************************************************************************
* 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.dsp.gain;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio gain that normalizes audio amplitude against an objective amplitude value where the audio amplitude values
* normally fall in the range of -1.0 to 1.0.
*
* Removes DC bias on a per audio sample buffer basis.
*
* This control is designed to work on a per-call basis by monitoring the maximum observed amplitude and adjusting the
* gain to normalize the amplitude toward the objective amplitude value. This control is designed to be reset at the
* beginning/end of each call segment so that the max observed amplitude is relevant to the current call segment.
*
* Min and max gains should be sized to constrain the applied value during periods of extended silence at the
* beginning of an audio segment or where a single amplitude spike might erroneously affect gain across the segment.
*/
public class AudioGainAndDcFilter
{
private static final Logger mLog = LoggerFactory.getLogger(AudioGainAndDcFilter.class);
private static final float MAX_AMPLITUDE = 0.95f;
private static final float GAIN_LOOP_BANDWIDTH = 0.0015f;
private float mMinGain;
private float mMaxGain;
private float mCurrentGain;
private float mObjectiveGain;
private float mObjectiveAmplitude;
private float mMaxObservedAmplitude;
/**
* Constructs an instance
* @param minGain to apply to the incoming sample stream.
* @param maxGain to apply
* @param objectiveAmplitude to achieve by adjusting gain between min and max.
*/
public AudioGainAndDcFilter(float minGain, float maxGain, float objectiveAmplitude)
{
mMinGain = minGain;
mMaxGain = maxGain;
mObjectiveAmplitude = objectiveAmplitude;
reset();
}
/**
* Resets this gain control to prepare for the next audio call/segment.
*/
public void reset()
{
mMaxObservedAmplitude = 0.0f;
mObjectiveGain = 1.0f;
mCurrentGain = 1.0f;
}
/**
* Process a buffer of audio samples and apply gain.
* @param samples to adjust.
* @return amplified audio samples
*/
public float[] process(float[] samples)
{
float currentAmplitude;
float dcAccumulator = 0.0f;
//Decay the max observed value by 10% each buffer so that an initial spike doesn't carry across all buffers
mMaxObservedAmplitude *= 0.9f;
for(float sample: samples)
{
dcAccumulator += sample;
currentAmplitude = FastMath.min(Math.abs(sample), 2.0f);
if(currentAmplitude > mMaxObservedAmplitude)
{
mMaxObservedAmplitude = currentAmplitude;
}
}
mObjectiveGain = mObjectiveAmplitude / mMaxObservedAmplitude;
if(mObjectiveGain > mMaxGain)
{
mObjectiveGain = mMaxGain;
}
else if(mObjectiveGain < mMinGain)
{
mObjectiveGain = mMinGain;
}
float dcOffset = dcAccumulator / samples.length;
float gain = mCurrentGain;
float objective = mObjectiveGain;
float[] processed = new float[samples.length];
boolean objectiveAchieved = (gain == objective);
float amplified;
for(int x = 0; x < samples.length; x++)
{
if(!objectiveAchieved)
{
gain += ((objective - gain) * GAIN_LOOP_BANDWIDTH);
if(Math.abs(objective - gain) < 0.00005f)
{
gain = objective;
objectiveAchieved = true;
}
}
amplified = (samples[x] - dcOffset) * gain;
amplified = FastMath.min(amplified, MAX_AMPLITUDE);
amplified = FastMath.max(amplified, -MAX_AMPLITUDE);
processed[x] = amplified;
}
mCurrentGain = gain;
mObjectiveGain = objective;
return processed;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,88 +19,80 @@
package io.github.dsheirer.dsp.gain;
import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.buffer.RealCircularBuffer;
import io.github.dsheirer.buffer.FloatCircularBuffer;
import io.github.dsheirer.source.wave.RealWaveSource;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
/**
* Audio automatic gain control.
*/
public class AutomaticGainControl
{
private static final double SAMPLE_RATE = 48000;
private static final Logger mLog = LoggerFactory.getLogger(AutomaticGainControl.class);
private static final float SAMPLE_RATE = 8000;
/* Signal delay line - time delay in seconds */
// private static final double DELAY_TIME_CONSTANT = 0.015;
private static final double DELAY_TIME_CONSTANT = 0.01;
private static final float DELAY_TIME_CONSTANT = 0.015f;
/* Peak detector window - time delay in seconds */
private static final double WINDOW_TIME_CONSTANT = 0.018;
private static final float WINDOW_TIME_CONSTANT = 0.018f;
/* Attack time constants in seconds */
private static final double ATTACK_RISE_TIME_CONSTANT = 0.002;
private static final double ATTACK_FALL_TIME_CONSTANT = 0.005;
private static final double ATTACK_RISE_ALPHA = 1.0 -
FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT);
private static final double ATTACK_FALL_ALPHA = 1.0 -
FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT);
private static final float ATTACK_RISE_TIME_CONSTANT = 0.002f;
private static final float ATTACK_FALL_TIME_CONSTANT = 0.005f;
private static final float ATTACK_RISE_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT);
private static final float ATTACK_FALL_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT);
/* AGC decay value in milliseconds (20 to 5000) */
private static final double DECAY = 200;
/* Ratio between rise and fall times of decay time constants - adjust for
* best action with SSB */
private static final double DECAY_RISEFALL_RATIO = 0.3;
private static final double DECAY_RISE_ALPHA = 1.0 -
FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO));
private static final double DECAY_FALL_ALPHA = 1.0 -
FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001));
/* Hang timer release decay time constant in seconds */
@SuppressWarnings("unused")
private static final double RELEASE_TIME_CONSTANT = 0.05;
/* Specifies the AGC Knee in dB if AGC is active (nominal range -160 to 0 dB) */
private static final double THRESHOLD = -100;
/* Limit output to about 3db of maximum */
private static final double AGC_OUT_SCALE = 0.7;
private static final float DECAY = 200.0f;
/* Ratio between rise and fall times of decay time constants - adjust for best action with SSB */
private static final float DECAY_RISEFALL_RATIO = 0.3f;
private static final float DECAY_RISE_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO));
private static final float DECAY_FALL_ALPHA = 1.f - (float)FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001));
/* Keep max input and output the same */
private static final double MAX_AMPLITUDE = 32767.0; //1.0;
private static final double MAX_MANUAL_AMPLITUDE = 32767.0; //1.0;
private static final float MAX_AMPLITUDE = 1.0f; //1.0;
private static final float MAX_MANUAL_AMPLITUDE = 1.0f; //1.0;
/* Specifies AGC manual gain in dB if AGC is not active ( 0 to 100 dB) */
private static final double MANUAL_GAIN = 0.0;
private static final double MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE *
FastMath.pow(10.0, MANUAL_GAIN / 20.0);
private static final float MANUAL_GAIN = 0.0f;
private static final float MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE * (float)FastMath.pow(10.0, MANUAL_GAIN / 20.0);
/* Limit output to about 3db of maximum */
private static final float AGC_OUT_SCALE = 0.95f; //0.7f;
/* Specifies dB reduction in output at knee from max output level (0 - 10dB) */
private static final double SLOPE_FACTOR = 2.0;
private static final double KNEE = THRESHOLD / 20.0;
private static final double GAIN_SLOPE = SLOPE_FACTOR / 100.0;
private static final double FIXED_GAIN = AGC_OUT_SCALE *
FastMath.pow(10.0, KNEE * (GAIN_SLOPE - 1.0));
private static final float SLOPE_FACTOR = 2.0f;
/* Specifies the threshold when the AGC kicks in (nominal range -160 to 0 dB) */
private static final float THRESHOLD = -20.0f; //-20.0f
private static final float KNEE = THRESHOLD / 20.0f; //20.0f
private static final float GAIN_SLOPE = SLOPE_FACTOR / 100.0f;
private static final float FIXED_GAIN = AGC_OUT_SCALE * (float)FastMath.pow(10.0, KNEE * (GAIN_SLOPE - 1.0));
/* Constant for calc log() so that a value of 0 magnitude = -8 */
private static final double MIN_CONSTANT = 3.2767E-4;
private static final float MIN_CONSTANT = 3.2767E-4f;
private boolean mAGCEnabled = true;
private float mPeakMagnitude = -5.0f;
private float mAttackAverage = 0.0f;
private float mDecayAverage = 0.0f;
private FloatCircularBuffer mDelayBuffer = new FloatCircularBuffer((int)(SAMPLE_RATE * DELAY_TIME_CONSTANT));
private FloatCircularBuffer mMagnitudeBuffer = new FloatCircularBuffer((int)(SAMPLE_RATE * WINDOW_TIME_CONSTANT), mPeakMagnitude);
private float mMaxGain = 0f;
private float mMinGain = 100000f;
private double mPeakMagnitude = 0.0;
private double mAttackAverage = 0.0;
private double mDecayAverage = 0.0;
private RealCircularBuffer mDelayBuffer = new RealCircularBuffer((int)(SAMPLE_RATE * DELAY_TIME_CONSTANT));
private DoubleCircularBuffer mMagnitudeBuffer = new DoubleCircularBuffer((int)(SAMPLE_RATE * WINDOW_TIME_CONSTANT));
/**
* Construct an instance
*/
public AutomaticGainControl()
{
System.out.println("Fixed gain: " + FIXED_GAIN + " Knee: " + KNEE + " Gain Slope:" + GAIN_SLOPE);
System.out.println("Manual gain: " + MANUAL_AGC_GAIN);
}
public void reset()
{
mLog.info("Previous - Min Gain: " + mMinGain + " Max Gain: " + mMaxGain);
mLog.info("Resetting - Peak:" + mPeakMagnitude + " Attack:" + mAttackAverage + " Decay:" + mDecayAverage + " Mag Max:" + mMagnitudeBuffer.max(mPeakMagnitude));
mPeakMagnitude = -8.0f;
mAttackAverage = -8.0f;
mDecayAverage = -8.0f;
mDelayBuffer.reset(0.0f);
mMagnitudeBuffer.reset(mPeakMagnitude);
}
/**
@@ -130,15 +122,14 @@ public class AutomaticGainControl
*/
public float process(float currentSample)
{
float delayedSample = mDelayBuffer.putAndGet(currentSample);
float delayedSample = mDelayBuffer.get(currentSample);
double gain = MANUAL_AGC_GAIN;
float gain = MANUAL_AGC_GAIN;
if(mAGCEnabled)
{
double currentMagnitude = FastMath.log10(FastMath.abs(currentSample) + MIN_CONSTANT) - FastMath.log10(MAX_AMPLITUDE);
double delayedMagnitude = mMagnitudeBuffer.get(currentMagnitude);
float currentMagnitude = (float)(FastMath.log10(FastMath.abs(currentSample) + MIN_CONSTANT) - FastMath.log10(MAX_AMPLITUDE));
float delayedMagnitude = mMagnitudeBuffer.get(currentMagnitude);
if(currentMagnitude > mPeakMagnitude)
{
@@ -148,29 +139,30 @@ public class AutomaticGainControl
else if(delayedMagnitude == mPeakMagnitude)
{
/* If delayed magnitude is the current peak, then find a new peak */
mPeakMagnitude = mMagnitudeBuffer.max();
mPeakMagnitude = mMagnitudeBuffer.max(-3.4845634f);
}
/* Exponential decay mode */
if(mPeakMagnitude > mAttackAverage)
{
mAttackAverage = ((1.0 - ATTACK_RISE_ALPHA) * mAttackAverage) + (ATTACK_RISE_ALPHA * mPeakMagnitude);
mAttackAverage = ((1.0f - ATTACK_RISE_ALPHA) * mAttackAverage) + (ATTACK_RISE_ALPHA * mPeakMagnitude);
}
else
{
mAttackAverage = ((1.0 - ATTACK_FALL_ALPHA) * mAttackAverage) + (ATTACK_FALL_ALPHA * mPeakMagnitude);
mAttackAverage = ((1.0f - ATTACK_FALL_ALPHA) * mAttackAverage) + (ATTACK_FALL_ALPHA * mPeakMagnitude);
}
if(mPeakMagnitude > mDecayAverage)
{
mDecayAverage = ((1.0 - DECAY_RISE_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
mDecayAverage = ((1.0f - DECAY_RISE_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
}
else
{
mDecayAverage = ((1.0 - DECAY_FALL_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
mDecayAverage = ((1.0f - DECAY_FALL_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
}
double magnitude = FastMath.max(mAttackAverage, mDecayAverage);
float magnitude = FastMath.max(mAttackAverage, mDecayAverage);
if(magnitude < KNEE)
{
@@ -178,11 +170,24 @@ public class AutomaticGainControl
}
else
{
gain = AGC_OUT_SCALE * FastMath.pow(10.0, magnitude * (GAIN_SLOPE - 1.0));
gain = AGC_OUT_SCALE * (float)FastMath.pow(10.0, magnitude * (GAIN_SLOPE - 1.0));
}
System.out.println("Current Mag: " + currentMagnitude + " Delay Mag: " + delayedMagnitude +
" Peak:" + mPeakMagnitude + " Gain:" + gain +
" Attack Avg:" + mAttackAverage + " Decay Avg:" + mDecayAverage);
}
return (float)(delayedSample * gain);
if(gain < mMinGain)
{
mMinGain = gain;
}
if(gain > mMaxGain)
{
mMaxGain = gain;
}
return delayedSample * gain;
}
/**
@@ -202,4 +207,5 @@ public class AutomaticGainControl
{
return mAGCEnabled;
}
}

View File

@@ -0,0 +1,28 @@
/*
* *****************************************************************************
* 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.dsp.magnitude;
/**
* Interface for implementations that calculate magnitude
*/
public interface IMagnitudeCalculator
{
float[] calculate(float[] i, float[] q);
}

View File

@@ -0,0 +1,59 @@
/*
* *****************************************************************************
* 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.dsp.magnitude;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for creating magnitude implementations
*/
public class MagnitudeFactory
{
private static final Logger mLog = LoggerFactory.getLogger(MagnitudeFactory.class);
/**
* Creates a magnitude implementation using calibration data to select the optimal
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static IMagnitudeCalculator getMagnitudeCalculator()
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.MAGNITUDE);
switch(implementation)
{
case VECTOR_SIMD_64:
return new VectorMagnitudeCalculator64();
case VECTOR_SIMD_128:
return new VectorMagnitudeCalculator128();
case VECTOR_SIMD_256:
return new VectorMagnitudeCalculator256();
case VECTOR_SIMD_512:
return new VectorMagnitudeCalculator512();
case SCALAR:
default:
return new ScalarMagnitudeCalculator();
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* *****************************************************************************
* 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.dsp.magnitude;
/**
* Scalar implementation of magnitude
*/
public class ScalarMagnitudeCalculator implements IMagnitudeCalculator
{
@Override
public float[] calculate(float[] i, float[] q)
{
float[] magnitude = new float[i.length];
for(int x = 0; x < i.length; x++)
{
magnitude[x] = ((i[x] * i[x]) + (q[x] * q[x]));
}
return magnitude;
}
}

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* 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.dsp.magnitude;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector implementation of magnitude calculation.
*/
public class VectorMagnitudeCalculator128 implements IMagnitudeCalculator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
@Override
public float[] calculate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
float[] magnitude = new float[i.length];
FloatVector iVector, qVector, result;
for(int x = 0; x < i.length; x += VECTOR_SPECIES.length())
{
iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x);
qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x);
result = iVector.mul(iVector).add(qVector.mul(qVector));
result.intoArray(magnitude, x);
}
return magnitude;
}
}

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* 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.dsp.magnitude;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector implementation of magnitude calculation.
*/
public class VectorMagnitudeCalculator256 implements IMagnitudeCalculator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
@Override
public float[] calculate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
float[] magnitude = new float[i.length];
FloatVector iVector, qVector, result;
for(int x = 0; x < i.length; x += VECTOR_SPECIES.length())
{
iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x);
qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x);
result = iVector.mul(iVector).add(qVector.mul(qVector));
result.intoArray(magnitude, x);
}
return magnitude;
}
}

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* 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.dsp.magnitude;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector implementation of magnitude calculation.
*/
public class VectorMagnitudeCalculator512 implements IMagnitudeCalculator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
@Override
public float[] calculate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
float[] magnitude = new float[i.length];
FloatVector iVector, qVector, result;
for(int x = 0; x < i.length; x += VECTOR_SPECIES.length())
{
iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x);
qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x);
result = iVector.mul(iVector).add(qVector.mul(qVector));
result.intoArray(magnitude, x);
}
return magnitude;
}
}

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* 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.dsp.magnitude;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector implementation of magnitude calculation.
*/
public class VectorMagnitudeCalculator64 implements IMagnitudeCalculator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
@Override
public float[] calculate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
float[] magnitude = new float[i.length];
FloatVector iVector, qVector, result;
for(int x = 0; x < i.length; x += VECTOR_SPECIES.length())
{
iVector = FloatVector.fromArray(VECTOR_SPECIES, i, x);
qVector = FloatVector.fromArray(VECTOR_SPECIES, q, x);
result = iVector.mul(iVector).add(qVector.mul(qVector));
result.intoArray(magnitude, x);
}
return magnitude;
}
}

View File

@@ -0,0 +1,324 @@
/*
* *****************************************************************************
* 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.dsp.squelch;
import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import java.time.Duration;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Adaptive squelch control that optionally tracks the noise floor and auto-adjusts squelch threshold.
*
* Initially modeled on: https://github.com/gnuradio/gnuradio/blob/master/gr-analog/lib/simple_squelch_cc_impl.cc
*/
public class AdaptiveSquelch implements Listener<SourceEvent>
{
private static final Logger mLog = LoggerFactory.getLogger(AdaptiveSquelch.class);
private static final long AUTO_ADJUST_PERIOD_MS = Duration.ofSeconds(5).toMillis();
private SinglePoleIirFilter mFilter;
private float mPower = 0.0f;
private boolean mSquelch;
private float mSquelchThreshold;
private Float mNoiseFloor;
private float mSquelchBuffer;
private boolean mSquelchChanged = false;
private int mPowerLevelBroadcastCount = 0;
private int mPowerLevelBroadcastThreshold;
private boolean mAutoTrackNoiseFloor;
private long mLastAutoAdjustTimestamp;
private Listener<SourceEvent> mSourceEventListener;
/**
* Constructs an instance
*
* Testing against a 12.5 kHz analog FM modulated signal, the following parameters provided a
* good responsiveness. A threshold of -80.0 dB seemed to trigger significant flapping during un-squelching.
* - alpha: 0.0001
* - threshold: 78.0 dB
*
* @param alpha decay value of the single pole IIR filter in range: 0.0 - 1.0. The smaller the alpha value,
* the slower the squelch response.
* @param squelchThreshold in decibels. Signal power must exceed this threshold value for unsquelch.
* @param autoTrack the noise floor when squelched and periodically readjust the squelch threshold.
*/
public AdaptiveSquelch(float alpha, float squelchThreshold, boolean autoTrack)
{
mFilter = new SinglePoleIirFilter(alpha);
setSquelchThreshold(squelchThreshold);
mAutoTrackNoiseFloor = autoTrack;
mLastAutoAdjustTimestamp = System.currentTimeMillis();
mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz, so 2x/second
}
/**
* Sets the sample rate to effect the frequency of power level notifications where the notifications are
* sent twice a second.
* @param sampleRate in hertz
*/
public void setSampleRate(int sampleRate)
{
mPowerLevelBroadcastThreshold = sampleRate / 2;
}
/**
* Squelch threshold value
* @return value in decibels
*/
public float getSquelchThreshold()
{
return toDb(mSquelchThreshold);
}
/**
* Formats the value as decibels
* @param value to format
* @return formatted value.
*/
private float toDb(float value)
{
return 10.0f * (float)FastMath.log10(value);
}
/**
* Converts from a decibel value
* @param valueDb in decibels
* @return not decibels
*/
private float fromDb(float valueDb)
{
return (float)FastMath.pow(10.0, valueDb / 10.0);
}
/**
* Enables or disables the squelch auto-tracking feature where the squelch threshold is auto-adjusted based on the
* observed channel power when the signal is not present (ie squelch state).
* @param autoTrack true to turn on auto-track
*/
public void setSquelchAutoTrack(boolean autoTrack)
{
mAutoTrackNoiseFloor = autoTrack;
broadcast(SourceEvent.squelchAutoTrack(getSquelchAutoTrack()));
}
/**
* Current squelch auto-track setting.
* @return true if auto-track is enabled.
*/
public boolean getSquelchAutoTrack()
{
return mAutoTrackNoiseFloor;
}
/**
* Sets the squelch threshold
* @param squelchThreshold in decibels
*/
public void setSquelchThreshold(float squelchThreshold)
{
float squelchThresholdDb = fromDb(squelchThreshold);
setSquelchThreshold(squelchThresholdDb, true);
}
/**
* Sets the squelch threshold. This method is synchronized to coordinate changes requested by the user/UI thread
* and auto-track changes made by the stream processing thread.
*
* @param squelchThreshold value
* @param resetNoiseFloor to reset the noise floor and squelch threshold
*/
private synchronized void setSquelchThreshold(float squelchThreshold, boolean resetNoiseFloor)
{
mSquelchThreshold = squelchThreshold;
if(resetNoiseFloor)
{
//Set noise floor to null so that both the noise floor and the squelch buffer get updated after first power reading
mNoiseFloor = null;
}
broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold()));
}
/**
* Sets the squelch state
* @param squelch true to squelch and false to unsquelch
*/
private void setSquelch(boolean squelch)
{
mSquelch = squelch;
mSquelchChanged = true;
}
/**
* Processes a complex IQ sample and changes squelch state when the signal power is above or below the
* threshold value.
* @param inphase complex sample component
* @param quadrature complex sample component
*/
public void process(float inphase, float quadrature)
{
process(inphase * inphase + quadrature * quadrature);
}
/**
* Processes a complex IQ sample and changes squelch state when the signal power is above or below the
* threshold value.
* @param magnitude of a complex sample (inphase * inphase + quadrature * quadrature)
*/
public void process(float magnitude)
{
mPower = mFilter.filter(magnitude);
if(mSquelch && mPower >= mSquelchThreshold)
{
setSquelch(false);
}
else if(!mSquelch && mPower < mSquelchThreshold)
{
setSquelch(true);
}
mPowerLevelBroadcastCount++;
if(mPowerLevelBroadcastCount % mPowerLevelBroadcastThreshold == 0)
{
mPowerLevelBroadcastCount = 0;
broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPower)));
if(mSquelch)
{
adjustSquelchThreshold();
}
}
}
/**
* With the user specified squelch threshold, establishes a squelch buffer (delta) from the actual noise floor and
* then automatically updates the squelch threshold to be the current noise floor plus this threshold value.
*
* Note: this method should only be invoked from a squelch (true) state.
*/
private void adjustSquelchThreshold()
{
if(mAutoTrackNoiseFloor)
{
if(mNoiseFloor == null)
{
//We set the squelch buffer just once, after the squelch threshold is set and noise floor is set to null
mSquelchBuffer = mSquelchThreshold - mPower;
}
mNoiseFloor = mPower;
if((System.currentTimeMillis() - mLastAutoAdjustTimestamp) > AUTO_ADJUST_PERIOD_MS)
{
setSquelchThreshold(mNoiseFloor + mSquelchBuffer, false);
mLastAutoAdjustTimestamp = System.currentTimeMillis();
}
}
}
/**
* Indicates if the current state is muted
*/
public boolean isMuted()
{
return mSquelch;
}
/**
* Indicates if the current state is unmuted
*/
public boolean isUnmuted()
{
return !mSquelch;
}
/**
* Current power level
* @return current power level in dB
*/
public float getPower()
{
return (float)(10.0 * Math.log10(mPower));
}
/**
* Indicates if the squelch state has changed (muted > unmuted, or vice-versa)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag
*/
public void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Registers the listener to receive power level notifications and squelch threshold requests
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mSourceEventListener = listener;
}
/**
* Broadcasts the source event to an optional register listener
*/
private void broadcast(SourceEvent event)
{
if(mSourceEventListener != null)
{
mSourceEventListener.receive(event);
}
}
/**
* Primary method to receive requests for squelch threshold change
*/
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
case REQUEST_CHANGE_SQUELCH_THRESHOLD:
setSquelchThreshold(sourceEvent.getValue().floatValue());
break;
case REQUEST_CURRENT_SQUELCH_THRESHOLD:
broadcast(SourceEvent.squelchThreshold(null, getSquelchThreshold()));
break;
case REQUEST_CHANGE_SQUELCH_AUTO_TRACK:
setSquelchAutoTrack(sourceEvent.getValue().intValue() == 1 ? true : false);
break;
case REQUEST_CURRENT_SQUELCH_AUTO_TRACK:
broadcast(SourceEvent.squelchAutoTrack(getSquelchAutoTrack()));
break;
}
}
}

View File

@@ -16,4 +16,16 @@ public interface ISquelchConfiguration
* @return threshold (dB)
*/
int getSquelchThreshold();
/**
* Enable or disable the squelch noise floor auto-track feature.
* @param autoTrack true to enable.
*/
void setSquelchAutoTrack(boolean autoTrack);
/**
* Indicates if the squelch noise floor auto-track feature is enabled.
* @return true if enabled.
*/
boolean isSquelchAutoTrack();
}

View File

@@ -1,8 +1,28 @@
/*
* *****************************************************************************
* 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.dsp.squelch;
import io.github.dsheirer.dsp.filter.iir.SinglePoleIirFilter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,8 +37,8 @@ public class PowerSquelch implements Listener<SourceEvent>
private static final Logger mLog = LoggerFactory.getLogger(PowerSquelch.class);
private State mState = State.MUTE;
private SinglePoleIirFilter mFilter;
private double mPower = 0.0f;
private double mSquelchThreshold;
private float mPower = 0.0f;
private float mSquelchThreshold;
private int mRampThreshold;
private int mRampCount;
private boolean mSquelchChanged = false;
@@ -42,7 +62,7 @@ public class PowerSquelch implements Listener<SourceEvent>
* @param ramp count of samples before transition between mute and unmute. Setting this value to zero
* causes immediate mute and unmute. Set to higher count to prevent mute/unmute flapping.
*/
public PowerSquelch(float alpha, double squelchThreshold, int ramp)
public PowerSquelch(float alpha, float squelchThreshold, int ramp)
{
mFilter = new SinglePoleIirFilter(alpha);
setSquelchThreshold(squelchThreshold);
@@ -64,9 +84,9 @@ public class PowerSquelch implements Listener<SourceEvent>
* Squelch threshold value
* @return value in decibels
*/
public double getSquelchThreshold()
public float getSquelchThreshold()
{
return 10.0 * Math.log10(mSquelchThreshold);
return 10.0f * (float)FastMath.log10(mSquelchThreshold);
}
/**
@@ -75,7 +95,7 @@ public class PowerSquelch implements Listener<SourceEvent>
*/
public void setSquelchThreshold(double squelchThreshold)
{
mSquelchThreshold = Math.pow(10.0, squelchThreshold / 10.0);
mSquelchThreshold = (float) FastMath.pow(10.0, squelchThreshold / 10.0);
broadcast(SourceEvent.squelchThreshold(null, squelchThreshold));
}
@@ -268,7 +288,7 @@ public class PowerSquelch implements Listener<SourceEvent>
{
if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
{
setSquelchThreshold(sourceEvent.getValue().doubleValue());
setSquelchThreshold(sourceEvent.getValue().floatValue());
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
{

View File

@@ -1,9 +1,27 @@
/*
* *****************************************************************************
* 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.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.BitSetFullException;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -14,7 +32,7 @@ import java.util.List;
public class BPTC_196_96
{
public static final int BPTC_LENGTH = 196;
public static final int EXTRACTED_LENGTH = 96;
public static final int EXTRACTED_LENGTH = 96; //However, we set the 3x reserved bits in 96, 97, and 98 making the length 99
public static final int MAX_ORIGINAL_INDEX = 136;
public static final int COLUMN_COUNT = 15;
public static final int MESSAGE_COLUMN_COUNT = 12; //Should be 11, but adjusted for the first pad bit
@@ -99,6 +117,11 @@ public class BPTC_196_96
}
}
//Transfer bits R2, R1, and R0 to the end - RAS bits
extracted.set(96, message.get(0));
extracted.set(97, message.get(1));
extracted.set(98, message.get(2));
return extracted;
}

View File

@@ -165,6 +165,8 @@ public class DMRRecordingViewer extends VBox
{
ioe.printStackTrace();
}
getMessageTableView().scrollTo(0);
}
}
@@ -364,7 +366,6 @@ public class DMRRecordingViewer extends VBox
if(selected != null)
{
mLog.info("Last Selected Directory updated to [" + selected.getParent() +"]");
mPreferences.put(LAST_SELECTED_DIRECTORY, selected.getParent());
load(selected);
}

View File

@@ -18,7 +18,7 @@
*/
package io.github.dsheirer.gui.instrument.chart;
import io.github.dsheirer.buffer.RealCircularBuffer;
import io.github.dsheirer.buffer.FloatCircularBuffer;
import io.github.dsheirer.sample.Listener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -34,7 +34,7 @@ public class RealSampleLineChart extends LineChart implements Listener<float[]>
private final static Logger mLog = LoggerFactory.getLogger(RealSampleLineChart.class);
private ObservableList<Data<Integer,Float>> mSamples = FXCollections.observableArrayList();
private Series<Integer,Float> mSampleSeries = new Series<>("Samples", mSamples);
private RealCircularBuffer mRealCircularBuffer;
private FloatCircularBuffer mRealCircularBuffer;
public RealSampleLineChart(int length, double tickUnit)
{
@@ -50,7 +50,7 @@ public class RealSampleLineChart extends LineChart implements Listener<float[]>
private void init(int length)
{
mRealCircularBuffer = new RealCircularBuffer(length);
mRealCircularBuffer = new FloatCircularBuffer(length);
for(int x = 0; x < length; x++)
{
@@ -74,7 +74,7 @@ public class RealSampleLineChart extends LineChart implements Listener<float[]>
mRealCircularBuffer.put(sample);
}
float[] bufferSamples = mRealCircularBuffer.get();
float[] bufferSamples = mRealCircularBuffer.getAll();
for(int x = 0; x < bufferSamples.length; x++)
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2021 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,15 +19,6 @@
package io.github.dsheirer.gui.playlist.alias;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasFactory;
import io.github.dsheirer.alias.AliasList;
@@ -37,6 +28,10 @@ import io.github.dsheirer.gui.playlist.Editor;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ObservableValue;
@@ -74,6 +69,9 @@ import javafx.util.Callback;
import jiconfont.IconCode;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Editor for aliases
@@ -532,11 +530,10 @@ public class AliasConfigurationEditor extends SplitPane
mDeleteAliasButton.setMaxWidth(Double.MAX_VALUE);
mDeleteAliasButton.setOnAction(event ->
{
boolean multiple = getAliasTableView().getSelectionModel().getSelectedItems().size() > 1;
int count = getAliasTableView().getSelectionModel().getSelectedItems().size();
Alert alert = new Alert(Alert.AlertType.CONFIRMATION,
"Do you want to delete the selected alias" + (multiple ? "es?" : "?"),
"Do you want to delete [" + count + "] selected alias" + ((count > 1) ? "es?" : "?"),
ButtonType.NO, ButtonType.YES);
alert.setTitle("Delete Alias");
alert.setHeaderText("Are you sure?");
@@ -547,11 +544,7 @@ public class AliasConfigurationEditor extends SplitPane
if(result.get() == ButtonType.YES)
{
List<Alias> selectedAliases = new ArrayList<>(getAliasTableView().getSelectionModel().getSelectedItems());
for(Alias selected : selectedAliases)
{
mPlaylistManager.getAliasModel().removeAlias(selected);
}
mPlaylistManager.getAliasModel().removeAliases(selectedAliases);
}
});
}

View File

@@ -677,6 +677,10 @@ public class AliasItemEditor extends Editor<Alias>
mAddIdentifierButton.setMaxWidth(Double.MAX_VALUE);
mAddIdentifierButton.setDisable(true);
Menu amMenu = new ProtocolMenu(Protocol.AM);
amMenu.getItems().add(new AddTalkgroupItem(Protocol.AM));
amMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.AM));
Menu p25Menu = new ProtocolMenu(Protocol.APCO25);
p25Menu.getItems().add(new AddTalkgroupItem(Protocol.APCO25));
p25Menu.getItems().add(new AddTalkgroupRangeItem(Protocol.APCO25));
@@ -712,6 +716,7 @@ public class AliasItemEditor extends Editor<Alias>
Menu nbfmMenu = new ProtocolMenu(Protocol.NBFM);
nbfmMenu.getItems().add(new AddTalkgroupItem(Protocol.NBFM));
nbfmMenu.getItems().add(new AddTalkgroupRangeItem(Protocol.NBFM));
Menu passportMenu = new ProtocolMenu(Protocol.PASSPORT);
passportMenu.getItems().add(new AddTalkgroupItem(Protocol.PASSPORT));
@@ -725,7 +730,7 @@ public class AliasItemEditor extends Editor<Alias>
Menu lojackMenu = new ProtocolMenu(Protocol.LOJACK);
lojackMenu.getItems().add(new AddLojackItem());
mAddIdentifierButton.getItems().addAll(p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu,
mAddIdentifierButton.getItems().addAll(amMenu, p25Menu, dmrMenu, fleetsyncMenu, ltrMenu, mdcMenu, mptMenu,
nbfmMenu, passportMenu, taitMenu, new SeparatorMenuItem(), lojackMenu);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,6 +27,8 @@ import io.github.dsheirer.gui.control.PrefixIdentFormatter;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.protocol.Protocol;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
@@ -38,9 +40,6 @@ import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Editor for talkgroup alias identifiers
*/
@@ -238,6 +237,10 @@ public class TalkgroupEditor extends IdentifierEditor<Talkgroup>
private void loadTalkgroupDetails()
{
mTalkgroupDetails.clear();
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.DECIMAL, new IntegerFormatter(1,0xFFFF),
"Format: 1 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.HEXADECIMAL, new HexFormatter(1,0xFFFF),
"Format: 1 - FFFF"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535),
"Format: 0 - 65535"));
mTalkgroupDetails.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,6 +27,8 @@ import io.github.dsheirer.gui.control.PrefixIdentFormatter;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.protocol.Protocol;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.HPos;
@@ -38,9 +40,6 @@ import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Editor for talkgroup range alias identifiers
*/
@@ -291,6 +290,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
private List<TalkgroupDetail> createTalkgroupDetails()
{
List<TalkgroupDetail> details = new ArrayList<>();
details.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535),
new IntegerFormatter(0,65535), "Format 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.AM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535),
new IntegerFormatter(0,65535), "Format 0 - FFFF"));
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535),
new IntegerFormatter(0,65535), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.APCO25, IntegerFormat.HEXADECIMAL, new HexFormatter(0,65535),
@@ -307,6 +310,10 @@ public class TalkgroupRangeEditor extends IdentifierEditor<TalkgroupRange>
details.add(new TalkgroupDetail(Protocol.MPT1327, IntegerFormat.FORMATTED,
new PrefixIdentFormatter(0,0xFFFFF), new PrefixIdentFormatter(0,0xFFFFF),
"Format: PPP-IIII = Prefix (0-127), Ident (1-8191)"));
details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.DECIMAL, new IntegerFormatter(0,65535),
new IntegerFormatter(0,65535), "Format 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.NBFM, IntegerFormat.HEXADECIMAL, new IntegerFormatter(0,65535),
new IntegerFormatter(0,65535), "Format 0 - FFFF"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.DECIMAL, new IntegerFormatter(0,0xFFFF),
new IntegerFormatter(0,0xFFFF), "Format: 0 - 65535"));
details.add(new TalkgroupDetail(Protocol.PASSPORT, IntegerFormat.HEXADECIMAL, new HexFormatter(0,0xFFFF),

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,25 +19,44 @@
package io.github.dsheirer.gui.playlist.channel;
import io.github.dsheirer.gui.control.DbPowerMeter;
import io.github.dsheirer.gui.control.HexFormatter;
import io.github.dsheirer.gui.control.IntegerFormatter;
import io.github.dsheirer.gui.playlist.eventlog.EventLogConfigurationEditor;
import io.github.dsheirer.gui.playlist.source.FrequencyEditor;
import io.github.dsheirer.gui.playlist.source.SourceConfigurationEditor;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.nbfm.DecodeConfigNBFM;
import io.github.dsheirer.module.log.EventLogType;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.text.TextAlignment;
import org.controlsfx.control.SegmentedButton;
import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -48,12 +67,23 @@ import org.slf4j.LoggerFactory;
public class AMConfigurationEditor extends ChannelConfigurationEditor
{
private final static Logger mLog = LoggerFactory.getLogger(AMConfigurationEditor.class);
private TitledPane mSourcePane;
private TitledPane mDecoderPane;
private TitledPane mRecordPane;
private TitledPane mSourcePane;
private ToggleSwitch mAudioRecordSwitch;
private TitledPane mEventLogPane;
private ToggleSwitch mBasebandRecordSwitch;
private SourceConfigurationEditor mSourceConfigurationEditor;
private SegmentedButton mBandwidthButton;
private TextField mTalkgroupField;
private TextField mSquelchThresholdField;
private ToggleSwitch mSquelchAutoTrackSwitch;
private TextFormatter<Integer> mTalkgroupTextFormatter;
private IntegerFormatter mSquelchTextFormatter = new IntegerFormatter((int)DbPowerMeter.DEFAULT_MINIMUM_POWER,
(int)DbPowerMeter.DEFAULT_MAXIMUM_POWER);
private EventLogConfigurationEditor mEventLogConfigurationEditor;
private TalkgroupValueChangeListener mTalkgroupValueChangeListener = new TalkgroupValueChangeListener();
private IntegerFormatter mDecimalFormatter = new IntegerFormatter(1, 65535);
private HexFormatter mHexFormatter = new HexFormatter(1, 65535);
/**
* Constructs an instance
@@ -67,6 +97,7 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
super(playlistManager, tunerManager, userPreferences);
getTitledPanesBox().getChildren().add(getSourcePane());
getTitledPanesBox().getChildren().add(getDecoderPane());
getTitledPanesBox().getChildren().add(getEventLogPane());
getTitledPanesBox().getChildren().add(getRecordPane());
}
@@ -93,13 +124,75 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
{
mDecoderPane = new TitledPane();
mDecoderPane.setText("Decoder: AM");
mDecoderPane.setExpanded(false);
mDecoderPane.setDisable(true);
mDecoderPane.setExpanded(true);
GridPane gridPane = new GridPane();
gridPane.setPadding(new Insets(10,10,10,10));
gridPane.setHgap(10);
gridPane.setVgap(10);
int row = 0;
Label bandwidthLabel = new Label("Channel Bandwidth");
GridPane.setHalignment(bandwidthLabel, HPos.RIGHT);
GridPane.setConstraints(bandwidthLabel, 0, row);
gridPane.getChildren().add(bandwidthLabel);
GridPane.setConstraints(getBandwidthButton(), 1, row++, 3, 1);
gridPane.getChildren().add(getBandwidthButton());
Label squelchLabel = new Label("Squelch Threshold");
GridPane.setHalignment(squelchLabel, HPos.RIGHT);
GridPane.setConstraints(squelchLabel, 0, row);
gridPane.getChildren().add(squelchLabel);
GridPane.setConstraints(getSquelchThresholdField(), 1, row);
gridPane.getChildren().add(getSquelchThresholdField());
Label autoTrackLabel = new Label("Squelch Auto-Track");
GridPane.setHalignment(autoTrackLabel, HPos.RIGHT);
GridPane.setConstraints(autoTrackLabel, 2, row);
gridPane.getChildren().add(autoTrackLabel);
GridPane.setConstraints(getSquelchAutoTrackSwitch(), 3, row);
GridPane.setHalignment(getSquelchAutoTrackSwitch(), HPos.LEFT);
gridPane.getChildren().add(getSquelchAutoTrackSwitch());
Label talkgroupLabel = new Label("Talkgroup To Assign");
GridPane.setHalignment(talkgroupLabel, HPos.RIGHT);
GridPane.setConstraints(talkgroupLabel, 4, row);
gridPane.getChildren().add(talkgroupLabel);
GridPane.setConstraints(getTalkgroupField(), 5, row);
gridPane.getChildren().add(getTalkgroupField());
mDecoderPane.setContent(gridPane);
//Special handling - the pill button doesn't like to set a selected state if the pane is not expanded,
//so detect when the pane is expanded and refresh the config view
mDecoderPane.expandedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue)
{
//Reset the config so the editor gets updated
setDecoderConfiguration(getItem().getDecodeConfiguration());
}
});
}
return mDecoderPane;
}
private TitledPane getEventLogPane()
{
if(mEventLogPane == null)
{
mEventLogPane = new TitledPane("Logging", getEventLogConfigurationEditor());
mEventLogPane.setExpanded(false);
}
return mEventLogPane;
}
private TitledPane getRecordPane()
{
if(mRecordPane == null)
@@ -113,14 +206,6 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
gridPane.setHgap(10);
gridPane.setVgap(10);
GridPane.setConstraints(getAudioRecordSwitch(), 0, 0);
gridPane.getChildren().add(getAudioRecordSwitch());
Label recordAudioLabel = new Label("Audio");
GridPane.setHalignment(recordAudioLabel, HPos.LEFT);
GridPane.setConstraints(recordAudioLabel, 1, 0);
gridPane.getChildren().add(recordAudioLabel);
GridPane.setConstraints(getBasebandRecordSwitch(), 0, 1);
gridPane.getChildren().add(getBasebandRecordSwitch());
@@ -149,17 +234,20 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
return mSourceConfigurationEditor;
}
private ToggleSwitch getAudioRecordSwitch()
private EventLogConfigurationEditor getEventLogConfigurationEditor()
{
if(mAudioRecordSwitch == null)
if(mEventLogConfigurationEditor == null)
{
mAudioRecordSwitch = new ToggleSwitch();
mAudioRecordSwitch.setDisable(true);
mAudioRecordSwitch.setTextAlignment(TextAlignment.RIGHT);
mAudioRecordSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
List<EventLogType> types = new ArrayList<>();
types.add(EventLogType.CALL_EVENT);
types.add(EventLogType.DECODED_MESSAGE);
mEventLogConfigurationEditor = new EventLogConfigurationEditor(types);
mEventLogConfigurationEditor.setPadding(new Insets(5,5,5,5));
mEventLogConfigurationEditor.modifiedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mAudioRecordSwitch;
return mEventLogConfigurationEditor;
}
private ToggleSwitch getBasebandRecordSwitch()
@@ -176,19 +264,179 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
return mBasebandRecordSwitch;
}
@Override
protected void setDecoderConfiguration(DecodeConfiguration config)
private SegmentedButton getBandwidthButton()
{
if(config instanceof DecodeConfigAM)
if(mBandwidthButton == null)
{
DecodeConfigAM decodeConfig = (DecodeConfigAM)config;
getAudioRecordSwitch().setDisable(false);
getAudioRecordSwitch().selectedProperty().set(decodeConfig.getRecordAudio());
mBandwidthButton = new SegmentedButton();
mBandwidthButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
mBandwidthButton.setDisable(true);
for(DecodeConfigNBFM.Bandwidth bandwidth : DecodeConfigNBFM.Bandwidth.AM_BANDWIDTHS)
{
ToggleButton toggleButton = new ToggleButton(bandwidth.toString());
toggleButton.setUserData(bandwidth);
mBandwidthButton.getButtons().add(toggleButton);
}
mBandwidthButton.getToggleGroup().selectedToggleProperty()
.addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
//Note: there is a weird timing bug with the segmented button where the toggles are not added to
//the toggle group until well after the control is rendered. We attempt to setItem() on the
//decode configuration and we're unable to correctly set the bandwidth setting. As a work
//around, we'll listen for the toggles to be added and update them here. This normally only
//happens when we first instantiate the editor and load an item for editing the first time.
mBandwidthButton.getToggleGroup().getToggles().addListener(new ListChangeListener<Toggle>()
{
@Override
public void onChanged(Change<? extends Toggle> c)
{
//This change event happens when the toggles are added -- we don't need to inspect the change event
if(getItem() != null && getItem().getDecodeConfiguration() instanceof DecodeConfigAM config)
{
//Capture current modified state so that we can reapply after adjusting control states
boolean modified = modifiedProperty().get();
DecodeConfigAM.Bandwidth bandwidth = config.getBandwidth();
if(bandwidth == null)
{
bandwidth = DecodeConfigNBFM.Bandwidth.BW_15_0;
}
for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles())
{
toggle.setSelected(toggle.getUserData() == bandwidth);
}
modifiedProperty().set(modified);
}
}
});
}
return mBandwidthButton;
}
private TextField getTalkgroupField()
{
if(mTalkgroupField == null)
{
mTalkgroupField = new TextField();
mTalkgroupField.setTextFormatter(mTalkgroupTextFormatter);
}
return mTalkgroupField;
}
private TextField getSquelchThresholdField()
{
if(mSquelchThresholdField == null)
{
mSquelchThresholdField = new TextField();
mSquelchThresholdField.setTextFormatter(mSquelchTextFormatter);
mSquelchTextFormatter.valueProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mSquelchThresholdField;
}
/**
* Squelch noise floor auto-track feature.
*/
private ToggleSwitch getSquelchAutoTrackSwitch()
{
if(mSquelchAutoTrackSwitch == null)
{
mSquelchAutoTrackSwitch = new ToggleSwitch();
mSquelchAutoTrackSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mSquelchAutoTrackSwitch;
}
/**
* Updates the talkgroup editor's text formatter.
* @param value to set in the control.
*/
private void updateTextFormatter(int value)
{
if(mTalkgroupTextFormatter != null)
{
mTalkgroupTextFormatter.valueProperty().removeListener(mTalkgroupValueChangeListener);
}
IntegerFormat format = mUserPreferences.getTalkgroupFormatPreference().getTalkgroupFormat(Protocol.AM);
if(format == null)
{
format = IntegerFormat.DECIMAL;
}
if(format == IntegerFormat.DECIMAL)
{
mTalkgroupTextFormatter = mDecimalFormatter;
getTalkgroupField().setTooltip(new Tooltip("1 - 65,535"));
}
else
{
getAudioRecordSwitch().setDisable(true);
getAudioRecordSwitch().selectedProperty().set(false);
mTalkgroupTextFormatter = mDecimalFormatter;
getTalkgroupField().setTooltip(new Tooltip("1 - FFFF"));
}
mTalkgroupTextFormatter.setValue(value);
getTalkgroupField().setTextFormatter(mTalkgroupTextFormatter);
mTalkgroupTextFormatter.valueProperty().addListener(mTalkgroupValueChangeListener);
}
/**
* Change listener to detect when talkgroup value has changed and set modified property to true.
*/
public class TalkgroupValueChangeListener implements ChangeListener<Integer>
{
@Override
public void changed(ObservableValue<? extends Integer> observable, Integer oldValue, Integer newValue)
{
modifiedProperty().set(true);
}
}
@Override
protected void setDecoderConfiguration(DecodeConfiguration config)
{
if(config instanceof DecodeConfigAM decodeConfigAM)
{
getBandwidthButton().setDisable(false);
final DecodeConfigAM.Bandwidth bandwidth = (decodeConfigAM.getBandwidth() != null ?
decodeConfigAM.getBandwidth() : DecodeConfigNBFM.Bandwidth.BW_15_0);
for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles())
{
toggle.setSelected(toggle.getUserData() == bandwidth);
}
updateTextFormatter(decodeConfigAM.getTalkgroup());
getSquelchThresholdField().setDisable(false);
mSquelchTextFormatter.setValue(decodeConfigAM.getSquelchThreshold());
getSquelchAutoTrackSwitch().setDisable(false);
getSquelchAutoTrackSwitch().setSelected(decodeConfigAM.isSquelchAutoTrack());
}
else
{
getBandwidthButton().setDisable(true);
for(Toggle toggle: getBandwidthButton().getToggleGroup().getToggles())
{
toggle.setSelected(false);
}
updateTextFormatter(0);
getTalkgroupField().setDisable(true);
getSquelchThresholdField().setDisable(true);
getSquelchAutoTrackSwitch().setDisable(true);
getSquelchAutoTrackSwitch().setSelected(false);
}
}
@@ -197,16 +445,34 @@ public class AMConfigurationEditor extends ChannelConfigurationEditor
{
DecodeConfigAM config;
if(getItem().getDecodeConfiguration() instanceof DecodeConfigAM)
if(getItem().getDecodeConfiguration() instanceof DecodeConfigAM decodeConfigAM)
{
config = (DecodeConfigAM)getItem().getDecodeConfiguration();
config = decodeConfigAM;
}
else
{
config = new DecodeConfigAM();
}
config.setRecordAudio(getAudioRecordSwitch().isSelected());
DecodeConfigAM.Bandwidth bandwidth = DecodeConfigAM.Bandwidth.BW_15_0;
if(getBandwidthButton().getToggleGroup().getSelectedToggle() != null)
{
bandwidth = (DecodeConfigAM.Bandwidth)getBandwidthButton().getToggleGroup().getSelectedToggle().getUserData();
}
config.setBandwidth(bandwidth);
Integer talkgroup = mTalkgroupTextFormatter.getValue();
if(talkgroup == null)
{
talkgroup = 1;
}
config.setTalkgroup(talkgroup);
config.setSquelchThreshold(mSquelchTextFormatter.getValue());
config.setSquelchAutoTrack(getSquelchAutoTrackSwitch().isSelected());
getItem().setDecodeConfiguration(config);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -40,6 +40,8 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
@@ -59,9 +61,6 @@ import org.controlsfx.control.ToggleSwitch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Narrow-Band FM channel configuration editor
*/
@@ -75,6 +74,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
private TitledPane mSourcePane;
private TextField mTalkgroupField;
private TextField mSquelchThresholdField;
private ToggleSwitch mSquelchAutoTrackSwitch;
private TextFormatter<Integer> mTalkgroupTextFormatter;
private IntegerFormatter mSquelchTextFormatter = new IntegerFormatter((int)DbPowerMeter.DEFAULT_MINIMUM_POWER,
(int)DbPowerMeter.DEFAULT_MAXIMUM_POWER);
@@ -151,12 +151,21 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
GridPane.setConstraints(getSquelchThresholdField(), 3, 0);
gridPane.getChildren().add(getSquelchThresholdField());
Label autoTrackLabel = new Label("Squelch Auto-Track");
GridPane.setHalignment(autoTrackLabel, HPos.RIGHT);
GridPane.setConstraints(autoTrackLabel, 4, 0);
gridPane.getChildren().add(autoTrackLabel);
GridPane.setConstraints(getSquelchAutoTrackSwitch(), 5, 0);
GridPane.setHalignment(getSquelchAutoTrackSwitch(), HPos.LEFT);
gridPane.getChildren().add(getSquelchAutoTrackSwitch());
Label talkgroupLabel = new Label("Talkgroup To Assign");
GridPane.setHalignment(talkgroupLabel, HPos.RIGHT);
GridPane.setConstraints(talkgroupLabel, 4, 0);
GridPane.setConstraints(talkgroupLabel, 6, 0);
gridPane.getChildren().add(talkgroupLabel);
GridPane.setConstraints(getTalkgroupField(), 5, 0);
GridPane.setConstraints(getTalkgroupField(), 7, 0);
gridPane.getChildren().add(getTalkgroupField());
mDecoderPane.setContent(gridPane);
@@ -274,7 +283,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
mBandwidthButton.getStyleClass().add(SegmentedButton.STYLE_CLASS_DARK);
mBandwidthButton.setDisable(true);
for(DecodeConfigNBFM.Bandwidth bandwidth : DecodeConfigNBFM.Bandwidth.values())
for(DecodeConfigNBFM.Bandwidth bandwidth : DecodeConfigNBFM.Bandwidth.FM_BANDWIDTHS)
{
ToggleButton toggleButton = new ToggleButton(bandwidth.toString());
toggleButton.setUserData(bandwidth);
@@ -338,11 +347,26 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
{
mSquelchThresholdField = new TextField();
mSquelchThresholdField.setTextFormatter(mSquelchTextFormatter);
mSquelchTextFormatter.valueProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mSquelchThresholdField;
}
/**
* Squelch noise floor auto-track feature.
*/
private ToggleSwitch getSquelchAutoTrackSwitch()
{
if(mSquelchAutoTrackSwitch == null)
{
mSquelchAutoTrackSwitch = new ToggleSwitch();
mSquelchAutoTrackSwitch.selectedProperty().addListener((observable, oldValue, newValue) -> modifiedProperty().set(true));
}
return mSquelchAutoTrackSwitch;
}
/**
* Updates the talkgroup editor's text formatter.
* @param value to set in the control.
@@ -421,8 +445,10 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
}
updateTextFormatter(decodeConfigNBFM.getTalkgroup());
getSquelchThresholdField().setDisable(false);
mSquelchTextFormatter.setValue(decodeConfigNBFM.getSquelchThreshold());
getSquelchAutoTrackSwitch().setDisable(false);
getSquelchAutoTrackSwitch().setSelected(decodeConfigNBFM.isSquelchAutoTrack());
}
else
{
@@ -435,6 +461,9 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
updateTextFormatter(0);
getTalkgroupField().setDisable(true);
getSquelchThresholdField().setDisable(true);
getSquelchAutoTrackSwitch().setDisable(true);
getSquelchAutoTrackSwitch().setSelected(false);
}
}
@@ -470,6 +499,7 @@ public class NBFMConfigurationEditor extends ChannelConfigurationEditor
config.setTalkgroup(talkgroup);
config.setSquelchThreshold(mSquelchTextFormatter.getValue());
config.setSquelchAutoTrack(getSquelchAutoTrackSwitch().isSelected());
getItem().setDecodeConfiguration(config);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -34,6 +34,12 @@ import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.rrapi.type.System;
import io.github.dsheirer.rrapi.type.Talkgroup;
import io.github.dsheirer.rrapi.type.TalkgroupCategory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import javafx.animation.RotateTransition;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -71,12 +77,6 @@ import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
public class SystemTalkgroupSelectionEditor extends GridPane
{
private static final Logger mLog = LoggerFactory.getLogger(SystemTalkgroupSelectionEditor.class);
@@ -323,13 +323,20 @@ public class SystemTalkgroupSelectionEditor extends GridPane
}
else
{
List<Talkgroup> aliasesToCreate = new ArrayList<>();
for(AliasedTalkgroup aliasedTalkgroup : mTalkgroupFilteredList)
{
if(!aliasedTalkgroup.hasAlias())
{
createAlias(aliasedTalkgroup.getTalkgroup());
aliasesToCreate.add(aliasedTalkgroup.getTalkgroup());
}
}
if(!aliasesToCreate.isEmpty())
{
createAliases(aliasesToCreate);
}
}
});
}
@@ -338,24 +345,31 @@ public class SystemTalkgroupSelectionEditor extends GridPane
}
/**
* Creates an alias for the specified talkgroup and adds it to the currently selected alias list
* @param talkgroup to alias
* Creates an alias for each of the specified talkgroups and adds it to the currently selected alias list
* @param talkgroups to alias
*/
public void createAlias(Talkgroup talkgroup)
public void createAliases(List<Talkgroup> talkgroups)
{
TalkgroupCategory talkgroupCategory = getTalkgroupCategory(talkgroup);
String group = (talkgroupCategory != null ? talkgroupCategory.getName() : null);
Alias alias = getRadioReferenceDecoder().createAlias(talkgroup, getCurrentSystem(),
getAliasListNameComboBox().getSelectionModel().getSelectedItem(), group);
List<Alias> createdAliases = new ArrayList<>();
if(getEncryptedAsDoNotMonitorCheckBox().selectedProperty().get() &&
TalkgroupEncryption.lookup(talkgroup.getEncryptionState()) == TalkgroupEncryption.FULL)
for(Talkgroup talkgroup: talkgroups)
{
int priority = io.github.dsheirer.alias.id.priority.Priority.DO_NOT_MONITOR;
alias.addAliasID(new io.github.dsheirer.alias.id.priority.Priority(priority));
TalkgroupCategory talkgroupCategory = getTalkgroupCategory(talkgroup);
String group = (talkgroupCategory != null ? talkgroupCategory.getName() : null);
Alias alias = getRadioReferenceDecoder().createAlias(talkgroup, getCurrentSystem(),
getAliasListNameComboBox().getSelectionModel().getSelectedItem(), group);
if(getEncryptedAsDoNotMonitorCheckBox().selectedProperty().get() &&
TalkgroupEncryption.lookup(talkgroup.getEncryptionState()) == TalkgroupEncryption.FULL)
{
int priority = io.github.dsheirer.alias.id.priority.Priority.DO_NOT_MONITOR;
alias.addAliasID(new io.github.dsheirer.alias.id.priority.Priority(priority));
}
createdAliases.add(alias);
}
mPlaylistManager.getAliasModel().addAlias(alias);
mPlaylistManager.getAliasModel().addAliases(createdAliases);
}
/**

View File

@@ -0,0 +1,234 @@
/*
* *****************************************************************************
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.streaming;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.controlsfx.control.ToggleSwitch;
/**
* RdioScanner calls API configuration editor
*/
public class RdioScannerEditor extends AbstractBroadcastEditor<RdioScannerConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(RdioScannerEditor.class);
private IntegerTextField mSystemIdTextField;
private IntegerTextField mMaxAgeTextField;
private TextField mApiKeyTextField;
private TextField mHostTextField;
private GridPane mEditorPane;
/**
* Constructs an instance
* @param playlistManager for accessing the broadcast model
*/
public RdioScannerEditor(PlaylistManager playlistManager)
{
super(playlistManager);
}
@Override
public void setItem(RdioScannerConfiguration item)
{
super.setItem(item);
getSystemIdTextField().setDisable(item == null);
getApiKeyTextField().setDisable(item == null);
getHostTextField().setDisable(item == null);
getMaxAgeTextField().setDisable(item == null);
if(item != null)
{
getSystemIdTextField().set(item.getSystemID());
getApiKeyTextField().setText(item.getApiKey());
getHostTextField().setText(item.getHost());
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
}
else
{
getSystemIdTextField().set(0);
getApiKeyTextField().setText(null);
getHostTextField().setText(null);
getMaxAgeTextField().set(0);
}
modifiedProperty().set(false);
}
@Override
public void dispose()
{
}
@Override
public void save()
{
if(getItem() != null)
{
int systemID = getSystemIdTextField().get() != null ? getSystemIdTextField().get() : 0;
getItem().setSystemID(systemID);
getItem().setHost(getHostTextField().getText());
getItem().setApiKey(getApiKeyTextField().getText());
getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
}
super.save();
}
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.RDIOSCANNER_CALL;
}
protected GridPane getEditorPane()
{
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 5, 10,10));
mEditorPane.setVgap(10);
mEditorPane.setHgap(5);
int row = 0;
Label formatLabel = new Label("Format");
GridPane.setHalignment(formatLabel, HPos.RIGHT);
GridPane.setConstraints(formatLabel, 0, row);
mEditorPane.getChildren().add(formatLabel);
GridPane.setConstraints(getFormatField(), 1, row);
mEditorPane.getChildren().add(getFormatField());
Label enabledLabel = new Label("Enabled");
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
GridPane.setConstraints(enabledLabel, 2, row);
mEditorPane.getChildren().add(enabledLabel);
GridPane.setConstraints(getEnabledSwitch(), 3, row);
mEditorPane.getChildren().add(getEnabledSwitch());
Label systemLabel = new Label("Name");
GridPane.setHalignment(systemLabel, HPos.RIGHT);
GridPane.setConstraints(systemLabel, 0, ++row);
mEditorPane.getChildren().add(systemLabel);
GridPane.setConstraints(getNameTextField(), 1, row);
mEditorPane.getChildren().add(getNameTextField());
Label apiKeyLabel = new Label("API Key");
GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
GridPane.setConstraints(apiKeyLabel, 0, ++row);
mEditorPane.getChildren().add(apiKeyLabel);
GridPane.setConstraints(getApiKeyTextField(), 1, row);
mEditorPane.getChildren().add(getApiKeyTextField());
Label systemIdLabel = new Label("System ID");
GridPane.setHalignment(systemIdLabel, HPos.RIGHT);
GridPane.setConstraints(systemIdLabel, 0, ++row);
mEditorPane.getChildren().add(systemIdLabel);
GridPane.setConstraints(getSystemIdTextField(), 1, row);
mEditorPane.getChildren().add(getSystemIdTextField());
Label hostLabel = new Label("RdioScanner URL");
GridPane.setHalignment(hostLabel, HPos.RIGHT);
GridPane.setConstraints(hostLabel, 0, ++row);
mEditorPane.getChildren().add(hostLabel);
GridPane.setConstraints(getHostTextField(), 1, row);
mEditorPane.getChildren().add(getHostTextField());
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
GridPane.setConstraints(maxAgeLabel, 0, ++row);
mEditorPane.getChildren().add(maxAgeLabel);
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
mEditorPane.getChildren().add(getMaxAgeTextField());
}
return mEditorPane;
}
private IntegerTextField getMaxAgeTextField()
{
if(mMaxAgeTextField == null)
{
mMaxAgeTextField = new IntegerTextField();
mMaxAgeTextField.setDisable(true);
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
}
return mMaxAgeTextField;
}
private TextField getHostTextField()
{
if(mHostTextField == null)
{
mHostTextField = new TextField();
mHostTextField.setDisable(true);
mHostTextField.textProperty().addListener(mEditorModificationListener);
}
return mHostTextField;
}
private TextField getApiKeyTextField()
{
if(mApiKeyTextField == null)
{
mApiKeyTextField = new TextField();
mApiKeyTextField.setDisable(true);
mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
}
return mApiKeyTextField;
}
private IntegerTextField getSystemIdTextField()
{
if(mSystemIdTextField == null)
{
mSystemIdTextField = new IntegerTextField();
mSystemIdTextField.setDisable(true);
mSystemIdTextField.textProperty().addListener(mEditorModificationListener);
}
return mSystemIdTextField;
}
}

View File

@@ -38,6 +38,8 @@ public class StreamEditorFactory
{
case BROADCASTIFY:
return new BroadcastifyStreamEditor(playlistManager);
case RDIOSCANNER_CALL:
return new RdioScannerEditor(playlistManager);
case BROADCASTIFY_CALL:
return new BroadcastifyCallEditor(playlistManager);
case ICECAST_HTTP:

View File

@@ -55,6 +55,7 @@ import org.slf4j.LoggerFactory;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JPanel;
@@ -94,6 +95,7 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
private JButton mSquelchUpButton;
private JButton mSquelchDownButton;
private JButton mLogIndexesButton;
private JCheckBox mSquelchAutoTrackCheckBox;
private double mSquelchThreshold;
private boolean mPanelVisible = false;
private boolean mDftProcessing = false;
@@ -114,10 +116,12 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
valuePanel.setLayout(new MigLayout("", "[right][left][][]", ""));
mPeakLabel = new JLabel("0");
mPeakLabel.setToolTipText("Current peak power level in decibels.");
valuePanel.add(new JLabel("Peak:"));
valuePanel.add(mPeakLabel, "wrap");
mPowerLabel = new JLabel("0");
mPowerLabel.setToolTipText("Current Power level in decibels");
valuePanel.add(new JLabel("Power:"));
valuePanel.add(mPowerLabel, "wrap");
@@ -125,21 +129,34 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mSquelchLabel.setEnabled(false);
valuePanel.add(mSquelchLabel);
mSquelchValueLabel = new JLabel(NOT_AVAILABLE);
mSquelchValueLabel.setToolTipText("Squelch threshold value in decibels");
mSquelchValueLabel.setEnabled(false);
valuePanel.add(mSquelchValueLabel, "wrap");
IconFontSwing.register(FontAwesome.getIconFont());
Icon iconUp = IconFontSwing.buildIcon(FontAwesome.ANGLE_UP, 12);
mSquelchUpButton = new JButton(iconUp);
mSquelchUpButton.setToolTipText("Increases the squelch threshold value");
mSquelchUpButton.setEnabled(false);
mSquelchUpButton.addActionListener(e -> broadcast(SourceEvent.requestSquelchThreshold(null, mSquelchThreshold + 1)));
valuePanel.add(mSquelchUpButton);
Icon iconDown = IconFontSwing.buildIcon(FontAwesome.ANGLE_DOWN, 12);
mSquelchDownButton = new JButton(iconDown);
mSquelchDownButton.setToolTipText("Decreases the squelch threshold value.");
mSquelchDownButton.setEnabled(false);
mSquelchDownButton.addActionListener(e -> broadcast(SourceEvent.requestSquelchThreshold(null, mSquelchThreshold - 1)));
valuePanel.add(mSquelchDownButton);
valuePanel.add(mSquelchDownButton, "wrap");
mSquelchAutoTrackCheckBox = new JCheckBox("Auto Track");
mSquelchAutoTrackCheckBox.setToolTipText("Enable or disable monitoring of the noise floor to auto-adjust the " +
"squelch threshold value maintaining a consistent level/buffer above the noise floor");
mSquelchAutoTrackCheckBox.setEnabled(false);
mSquelchAutoTrackCheckBox.addActionListener(e ->
{
broadcast(SourceEvent.requestSquelchAutoTrack(mSquelchAutoTrackCheckBox.isSelected()));
});
valuePanel.add(mSquelchAutoTrackCheckBox, "span,left");
add(valuePanel);
add(new JSeparator(JSeparator.VERTICAL));
@@ -165,7 +182,7 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
});
mNoiseFloorSpinner = new JSpinner(mNoiseFloorSpinnerModel);
labelPanel.add(mNoiseFloorSpinner);
labelPanel.add(new JLabel("Noise Floor"));
labelPanel.add(new JLabel("Spectral Display Noise Floor"));
mLogIndexesButton = new JButton("Log Settings");
mLogIndexesButton.addActionListener(e -> {
@@ -340,6 +357,22 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
}
}
/**
* Updates the channel configuration squelch auto-track feature setting.
* @param autoTrack true to enable.
*/
private void setConfigSquelchAutoTrack(boolean autoTrack)
{
Channel channel = mPlaylistManager.getChannelProcessingManager().getChannel(mProcessingChain);
if(channel != null && channel.getDecodeConfiguration() instanceof ISquelchConfiguration)
{
ISquelchConfiguration configuration = (ISquelchConfiguration)channel.getDecodeConfiguration();
configuration.setSquelchAutoTrack(autoTrack);
mPlaylistManager.schedulePlaylistSave();
}
}
private void broadcast(SourceEvent sourceEvent)
{
if(mProcessingChain != null)
@@ -372,6 +405,8 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
mSquelchValueLabel.setEnabled(false);
mSquelchUpButton.setEnabled(false);
mSquelchDownButton.setEnabled(false);
mSquelchAutoTrackCheckBox.setEnabled(false);
mSquelchAutoTrackCheckBox.setSelected(false);
}
/**
@@ -420,7 +455,8 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
updateFFTProcessing();
broadcast(SourceEvent.requestCurrentSquelchThreshold(null));
broadcast(SourceEvent.requestCurrentSquelchAutoTrack());
broadcast(SourceEvent.requestCurrentSquelchThreshold());
}
@@ -475,6 +511,15 @@ public class ChannelPowerPanel extends JPanel implements Listener<ProcessingChai
updatePllFrequency(sourceEvent.getValue().longValue());
}
}
case NOTIFICATION_SQUELCH_AUTO_TRACK ->
{
boolean autoTrack = sourceEvent.getValue().intValue() == 1 ? true : false;
setConfigSquelchAutoTrack(autoTrack);
EventQueue.invokeLater(() -> {
mSquelchAutoTrackCheckBox.setSelected(autoTrack);
mSquelchAutoTrackCheckBox.setEnabled(true);
});
}
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* *****************************************************************************
* 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.log;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
/**
* Logging suppressor that squelches log messages after they have been logged and continue to occur.
*/
public class LoggingSuppressor
{
private Map<String,Integer> mSuppressionCountMap = new HashMap<>();
private Logger mLogger;
/**
* Constructs an instance
* @param logger to receive suppressed log messages.
*/
public LoggingSuppressor(Logger logger)
{
mLogger = logger;
}
/**
* Logs the information message.
* @param key for suppression.
* @param maxCount of the logged instance.
* @param message to log.
*/
public void info(String key, int maxCount, String message)
{
if(canLog(key, maxCount))
{
mLogger.info(message + getTag(key, maxCount));
}
}
/**
* Logs the information message.
* @param key for suppression.
* @param maxCount of the logged instance.
* @param message to log.
* @param t stack trace to include.
*/
public void info(String key, int maxCount, String message, Throwable t)
{
if(canLog(key, maxCount))
{
mLogger.info(message + getTag(key, maxCount), t);
}
}
/**
* Logs the error message.
* @param key for suppression.
* @param maxCount of the logged instance.
* @param message to log.
*/
public void error(String key, int maxCount, String message)
{
if(canLog(key, maxCount))
{
mLogger.error(message + getTag(key, maxCount));
}
}
/**
* Logs the error message.
* @param key for suppression.
* @param maxCount of the logged instance.
* @param message to log.
* @param t stack trace to include.
*/
public void error(String key, int maxCount, String message, Throwable t)
{
if(canLog(key, maxCount))
{
mLogger.error(message + getTag(key, maxCount), t);
}
}
/**
* Creates a logging tag that logs the suppression statistics.
* @param key for retrieving current count.
* @param maxCount to log.
* @return logging tag
*/
private String getTag(String key, int maxCount)
{
int count = mSuppressionCountMap.get(key);
return " [Log Suppress " + count + "/" + maxCount + "]";
}
/**
* Indicates if the operation can be logged without exceeding the max logging count specified by the user.
* @param key for tracking by count
* @param maxCount of the key to log
* @return true if can log or false.
*/
private boolean canLog(String key, int maxCount)
{
if(mSuppressionCountMap.containsKey(key))
{
int count = mSuppressionCountMap.get(key);
if(count >= maxCount)
{
return false;
}
mSuppressionCountMap.put(key, count + 1);
}
else
{
mSuppressionCountMap.put(key, 1);
}
return true;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.alias.action.AliasActionManager;
import io.github.dsheirer.audio.AbstractAudioModule;
import io.github.dsheirer.audio.AudioModule;
import io.github.dsheirer.channel.state.AlwaysUnsquelchedDecoderState;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.Channel.ChannelType;
@@ -36,6 +35,7 @@ import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.MessageDirection;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.decode.am.AMDecoder;
import io.github.dsheirer.module.decode.am.AMDecoderState;
import io.github.dsheirer.module.decode.am.DecodeConfigAM;
import io.github.dsheirer.module.decode.config.AuxDecodeConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
@@ -88,23 +88,20 @@ import io.github.dsheirer.module.decode.passport.PassportMessageFilter;
import io.github.dsheirer.module.decode.tait.Tait1200Decoder;
import io.github.dsheirer.module.decode.tait.Tait1200DecoderState;
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
import io.github.dsheirer.module.demodulate.am.AMDemodulatorModule;
import io.github.dsheirer.module.demodulate.fm.FMDemodulatorModule;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.tuner.channel.rotation.ChannelRotationMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class DecoderFactory
{
private final static Logger mLog = LoggerFactory.getLogger(DecoderFactory.class);
private static final double AM_CHANNEL_BANDWIDTH = 3000.0;
private static final double FM_CHANNEL_BANDWIDTH = 12500.0;
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
@@ -313,29 +310,23 @@ public class DecoderFactory
}
DecodeConfigNBFM decodeConfigNBFM = (DecodeConfigNBFM)decodeConfig;
modules.add(new NBFMDecoder(decodeConfigNBFM));
modules.add(new NBFMDecoderState(channel.getName(), decodeConfigNBFM));
modules.add(new AudioModule(aliasList));
}
private static void processAM(Channel channel, List<Module> modules, AliasList aliasList, DecodeConfiguration decodeConfig) {
modules.add(new AMDecoder(decodeConfig));
modules.add(new AlwaysUnsquelchedDecoderState(DecoderType.AM, channel.getName()));
AudioModule audioModuleAM = new AudioModule(aliasList);
modules.add(audioModuleAM);
//Check if the user wants all audio recorded ..
if(((DecodeConfigAM)decodeConfig).getRecordAudio())
if(!(decodeConfig instanceof DecodeConfigAM))
{
audioModuleAM.setRecordAudio(true);
throw new IllegalArgumentException("Can't create AM decoder - unrecognized decode config type: " +
(decodeConfig != null ? decodeConfig.getClass() : "null/empty"));
}
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
{
modules.add(new AMDemodulatorModule(AM_CHANNEL_BANDWIDTH, DEMODULATED_AUDIO_SAMPLE_RATE));
}
DecodeConfigAM decodeConfigAM = (DecodeConfigAM) decodeConfig;
modules.add(new AMDecoder(decodeConfigAM));
modules.add(new AMDecoderState(channel.getName(), decodeConfigAM));
modules.add(new AudioModule(aliasList));
}
/**
@@ -506,8 +497,6 @@ public class DecoderFactory
public static DecodeConfiguration getDecodeConfiguration(DecoderType decoder)
{
DecodeConfiguration retVal;
switch(decoder)
{
case AM:
@@ -545,7 +534,10 @@ public class DecoderFactory
case AM:
DecodeConfigAM copyAM = new DecodeConfigAM();
DecodeConfigAM origAM = (DecodeConfigAM)config;
copyAM.setRecordAudio(origAM.getRecordAudio());
copyAM.setBandwidth(origAM.getBandwidth());
copyAM.setTalkgroup(origAM.getTalkgroup());
copyAM.setSquelchThreshold(origAM.getSquelchThreshold());
copyAM.setSquelchAutoTrack(origAM.isSquelchAutoTrack());
return copyAM;
case DMR:
return new DecodeConfigDMR();
@@ -571,6 +563,9 @@ public class DecoderFactory
DecodeConfigNBFM origNBFM = (DecodeConfigNBFM)config;
DecodeConfigNBFM copyNBFM = new DecodeConfigNBFM();
copyNBFM.setBandwidth(origNBFM.getBandwidth());
copyNBFM.setSquelchThreshold(origNBFM.getSquelchThreshold());
copyNBFM.setSquelchAutoTrack(origNBFM.isSquelchAutoTrack());
copyNBFM.setTalkgroup(origNBFM.getTalkgroup());
return copyNBFM;
case P25_PHASE1:
DecodeConfigP25Phase1 originalP25 = (DecodeConfigP25Phase1)config;

View File

@@ -1,52 +1,75 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 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/>
******************************************************************************/
/*
* *****************************************************************************
* 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.am;
import io.github.dsheirer.dsp.am.SquelchingAMDemodulator;
import io.github.dsheirer.dsp.gain.AudioGainAndDcFilter;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.PrimaryDecoder;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.analog.SquelchingAnalogDecoder;
public class AMDecoder extends PrimaryDecoder
/**
* Analog AM radio decoder module with integrated squelching control
*/
public class AMDecoder extends SquelchingAnalogDecoder
{
public AMDecoder( DecodeConfiguration config )
{
super( config );
}
private static final float DEMODULATOR_GAIN = 150.0f;
private static final float SQUELCH_ALPHA_DECAY = 0.0004f;
private static final float MINIMUM_GAIN = 0.5f;
private static final float MAXIMUM_GAIN = 16.0f;
private static final float OBJECTIVE_AUDIO_AMPLITUDE = 0.75f;
private AudioGainAndDcFilter mAGC = new AudioGainAndDcFilter(MINIMUM_GAIN, MAXIMUM_GAIN, OBJECTIVE_AUDIO_AMPLITUDE);
@Override
public DecoderType getDecoderType()
/**
* Constructs an instance
* @param config for the AM channel.
*/
public AMDecoder(DecodeConfigAM config)
{
return DecoderType.AM;
super(config, new SquelchingAMDemodulator(DEMODULATOR_GAIN, SQUELCH_ALPHA_DECAY,
config.getSquelchThreshold(), config.isSquelchAutoTrack()));
}
@Override
public void reset()
{
}
@Override
public void start()
public DecoderType getDecoderType()
{
return DecoderType.AM;
}
@Override
public void stop()
{
}
/**
* Overrides the broadcast of the final demodulated audio samples so that we can apply AGC
* @param demodulatedSamples to broadcast
*/
@Override
protected void broadcast(float[] demodulatedSamples)
{
//Apply audio gain and rebroadcast
super.broadcast(mAGC.process(demodulatedSamples));
}
/**
* Overrides the notify call start so that we can reset the AGC
*/
@Override
protected void notifyCallStart()
{
mAGC.reset();
super.notifyCallStart();
}
}

View File

@@ -0,0 +1,79 @@
/*
* *****************************************************************************
* 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.am;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.analog.AnalogDecoderState;
/**
* AM decoder state
*/
public class AMDecoderState extends AnalogDecoderState
{
private String mChannelName;
private Identifier mChannelNameIdentifier;
private Identifier mTalkgroupIdentifier;
/**
* Constructs an instance
* @param channelName to use for this channel
* @param decodeConfig with talkgroup identifier
*/
public AMDecoderState(String channelName, DecodeConfigAM decodeConfig)
{
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : "AM CHANNEL";
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.CONFIGURATION, Form.CHANNEL_NAME, Role.ANY);
mTalkgroupIdentifier = new AMTalkgroup(decodeConfig.getTalkgroup());
}
@Override
public DecoderType getDecoderType()
{
return DecoderType.AM;
}
@Override
protected Identifier getChannelNameIdentifier()
{
return mChannelNameIdentifier;
}
@Override
protected Identifier getTalkgroupIdentifier()
{
return mTalkgroupIdentifier;
}
@Override
public String getActivitySummary()
{
StringBuilder sb = new StringBuilder();
sb.append("Activity Summary\n");
sb.append("\tDecoder: AM");
sb.append("\n\n");
return sb.toString();
}
}

View File

@@ -0,0 +1,46 @@
/*
* *****************************************************************************
* 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.am;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.protocol.Protocol;
/**
* AM talkgroup. Note: this is a logical identifier that is assignable from a user-specified
* configuration so that AM audio events are compatible with other features of the sdrtrunk system.
*/
public class AMTalkgroup extends TalkgroupIdentifier
{
/**
* Constructs an instance
* @param value 1-65,535
*/
public AMTalkgroup(int value)
{
super(value, Role.TO);
}
@Override
public Protocol getProtocol()
{
return Protocol.AM;
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,33 +14,35 @@
*
* 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.am;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.analog.DecodeConfigAnalog;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
public class DecodeConfigAM extends DecodeConfiguration
/**
* AM decoder configuration
*/
public class DecodeConfigAM extends DecodeConfigAnalog
{
private boolean mRecordAudio = false;
public DecodeConfigAM()
{
}
@JacksonXmlProperty(isAttribute = true, localName = "recordAudio")
public boolean getRecordAudio()
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
public DecoderType getDecoderType()
{
return mRecordAudio;
return DecoderType.AM;
}
public void setRecordAudio(boolean recordAudio)
@Override
protected Bandwidth getDefaultBandwidth()
{
mRecordAudio = recordAudio;
return Bandwidth.BW_15_0;
}
/**
@@ -51,13 +52,20 @@ public class DecodeConfigAM extends DecodeConfiguration
@Override
public ChannelSpecification getChannelSpecification()
{
return new ChannelSpecification(25000.0,
3000, 3000.0, 5000.0);
}
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
public DecoderType getDecoderType()
{
return DecoderType.AM;
switch(getBandwidth())
{
case BW_3_0:
return new ChannelSpecification(25000.0, 3000, 1500.0, 1700.0);
case BW_5_0:
return new ChannelSpecification(25000.0, 5000, 2500.0, 2700.0);
case BW_8_33:
return new ChannelSpecification(25000.0, 10000, 5000.0, 7000.0);
case BW_15_0:
return new ChannelSpecification(25000.0, 15000, 7500.0, 9500.0);
case BW_25_0:
return new ChannelSpecification(25000.0, 25000, 12500.0, 14500.0);
default:
throw new IllegalArgumentException("Unrecognized AM bandwidth value: " + getBandwidth());
}
}
}

View File

@@ -0,0 +1,177 @@
/*
* *****************************************************************************
* 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.analog;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.state.DecoderState;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.p25.identifier.channel.StandardChannel;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract analog decoder channel state - provides the minimum channel state functionality
*/
public abstract class AnalogDecoderState extends DecoderState implements ISourceEventListener
{
private final static Logger mLog = LoggerFactory.getLogger(AnalogDecoderState.class);
private Listener<SourceEvent> mSourceEventListener = new SourceEventListener();
private DecodeEvent mDecodeEvent;
private IChannelDescriptor mChannelDescriptor = null;
public AnalogDecoderState()
{
}
/**
* Channel name identifier provided by the subclass implementation.
*/
protected abstract Identifier getChannelNameIdentifier();
protected abstract Identifier getTalkgroupIdentifier();
@Override
public void receiveDecoderStateEvent(DecoderStateEvent event)
{
switch(event.getEvent())
{
case REQUEST_RESET ->
{
getIdentifierCollection().update(getChannelNameIdentifier());
}
case START ->
{
if(event.getState() == State.CALL)
{
startCallEvent();
}
}
case END ->
{
if(event.getState() == State.CALL)
{
endCallEvent();
}
}
case CONTINUATION ->
{
if(event.getState() == State.CALL)
{
continueCallEvent();
}
else
{
endCallEvent();
}
}
}
}
/**
* Creates/starts a decode call evnet.
*/
private void startCallEvent()
{
getIdentifierCollection().update(getTalkgroupIdentifier());
if(mDecodeEvent == null)
{
mDecodeEvent = DecodeEvent.builder(System.currentTimeMillis())
.channel(mChannelDescriptor)
.eventDescription("CALL")
.details(getDecoderType().name())
.identifiers(new IdentifierCollection(getIdentifierCollection().getIdentifiers()))
.build();
broadcast(mDecodeEvent);
}
}
/**
* Continues (or starts) the call decode event and updates the current timestamp
*/
private void continueCallEvent()
{
if(mDecodeEvent == null)
{
startCallEvent();
}
mDecodeEvent.update(System.currentTimeMillis());
broadcast(mDecodeEvent);
}
/**
* Ends the call decode event
*/
private void endCallEvent()
{
if(mDecodeEvent != null)
{
mDecodeEvent.end(System.currentTimeMillis());
broadcast(mDecodeEvent);
mDecodeEvent = null;
}
//Remove any user identifiers from the identifier collection
resetState();
}
@Override
public void start()
{
getIdentifierCollection().update(getChannelNameIdentifier());
}
@Override
public void stop() {}
@Override
public void init() {}
@Override
public void receive(IMessage t) {}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventListener;
}
/**
* Monitors source events to capture the channel frequency for use in decode events.
*/
private class SourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_FREQUENCY_CHANGE)
{
mChannelDescriptor = new StandardChannel(sourceEvent.getValue().longValue());
}
}
}
}

View File

@@ -0,0 +1,207 @@
/*
* *****************************************************************************
* 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.analog;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.dsp.squelch.ISquelchConfiguration;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.source.tuner.channel.ChannelSpecification;
import java.util.EnumSet;
/**
* Decoder configuration for an squelching analog channel
*/
public abstract class DecodeConfigAnalog extends DecodeConfiguration implements ISquelchConfiguration
{
private Bandwidth mBandwidth;
private int mTalkgroup = 1;
private int mSquelchThreshold = -78;
private boolean mSquelchAutoTrack = true;
/**
* Constructs an instance
*/
public DecodeConfigAnalog()
{
mBandwidth = getDefaultBandwidth();
}
protected abstract Bandwidth getDefaultBandwidth();
/**
* Channel bandwidth
*/
@JacksonXmlProperty(isAttribute = true, localName = "bandwidth")
public Bandwidth getBandwidth()
{
return mBandwidth;
}
@JsonIgnore
@Override
public ChannelSpecification getChannelSpecification()
{
if(mBandwidth == Bandwidth.BW_12_5)
{
return new ChannelSpecification(25000.0, 12500, 6000.0, 7000.0);
}
else
{
return new ChannelSpecification(50000.0, 25000, 12500.0, 13500.0);
}
}
/**
* Sets the channel bandwidth
*/
public void setBandwidth(Bandwidth bandwidth)
{
mBandwidth = bandwidth;
}
/**
* Talkgroup to associate with audio produced by the NBFM decoder
*/
@JacksonXmlProperty(isAttribute = true, localName = "talkgroup")
public int getTalkgroup()
{
return mTalkgroup;
}
/**
* Sets the talkgroup identifier to attach to any demodulated audio streams
* @param talkgroup (1-65,535)
*/
public void setTalkgroup(int talkgroup)
{
if(talkgroup < 1 || talkgroup > 65535)
{
throw new IllegalArgumentException("Valid talkgroup range is 1 - 65,535");
}
mTalkgroup = talkgroup;
}
/**
* Sets squelch threshold
* @param threshold (dB)
*/
@JacksonXmlProperty(isAttribute = true, localName = "squelch")
@Override
public void setSquelchThreshold(int threshold)
{
mSquelchThreshold = threshold;
}
/**
* Squelch threshold
* @return threshold (dB)
*/
@Override
public int getSquelchThreshold()
{
return mSquelchThreshold;
}
/**
* Enable or disable the squelch noise floor auto-track feature.
* @param autoTrack true to enable.
*/
@JacksonXmlProperty(isAttribute = true, localName = "autoTrack")
@Override
public void setSquelchAutoTrack(boolean autoTrack)
{
mSquelchAutoTrack = autoTrack;
}
/**
* Indicates if the squelch noise floor auto-track feature is enabled.
* @return true if enabled.
*/
@Override
public boolean isSquelchAutoTrack()
{
return mSquelchAutoTrack;
}
public enum Bandwidth
{
BW_3_0("3.0 kHz", 3000.0),
BW_5_0("5.0 kHz", 5000.0),
BW_8_33("8.33 kHz", 8333.0),
BW_12_5("12.5 kHz", 12500.0),
BW_15_0("15.0 kHz", 15000.0),
BW_25_0("25.0 kHz", 25000.0);
private String mLabel;
private double mValue;
/**
* Constructs an instance
* @param label for display
* @param value of the entry in Hertz
*/
Bandwidth(String label, double value)
{
mLabel = label;
mValue = value;
}
//AM demodulator channel bandwidth options
public static EnumSet<Bandwidth> AM_BANDWIDTHS = EnumSet.of(BW_3_0, BW_5_0, BW_8_33, BW_15_0, BW_25_0);
//FM demodulator channel bandwidth options
public static EnumSet<Bandwidth> FM_BANDWIDTHS = EnumSet.of(BW_12_5, BW_25_0);
/**
* Indicates if this entry is valid for the AM decoder.
*/
public boolean isAM()
{
return AM_BANDWIDTHS.contains(this);
}
/**
* Indicates if this entry is valid for the FM decoder.
*/
public boolean isFM()
{
return FM_BANDWIDTHS.contains(this);
}
/**
* Overrides the default toString to provide a pretty label.
*/
@Override
public String toString()
{
return mLabel;
}
/**
* Value of the bandwidth
* @return value in Hertz
*/
public double getValue()
{
return mValue;
}
}
}

View File

@@ -0,0 +1,410 @@
/*
* *****************************************************************************
* 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.analog;
import io.github.dsheirer.channel.state.DecoderStateEvent;
import io.github.dsheirer.channel.state.IDecoderStateEventProvider;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.decimate.DecimationFilterFactory;
import io.github.dsheirer.dsp.filter.decimate.IRealDecimationFilter;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.resample.RealResampler;
import io.github.dsheirer.dsp.fm.ISquelchingDemodulator;
import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.module.decode.PrimaryDecoder;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.IComplexSamplesListener;
import io.github.dsheirer.sample.real.IRealBufferProvider;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract analog decoder module with integrated squelch control
*/
public abstract class SquelchingAnalogDecoder extends PrimaryDecoder implements ISourceEventListener, ISourceEventProvider,
IComplexSamplesListener, Listener<ComplexSamples>, IRealBufferProvider,
IDecoderStateEventProvider
{
private final static Logger mLog = LoggerFactory.getLogger(SquelchingAnalogDecoder.class);
private static final double DEMODULATED_AUDIO_SAMPLE_RATE = 8000.0;
private IRealFilter mIBasebandFilter;
private IRealFilter mQBasebandFilter;
private IRealDecimationFilter mIDecimationFilter;
private IRealDecimationFilter mQDecimationFilter;
private ISquelchingDemodulator mDemodulator;
private RealResampler mResampler;
private SourceEventProcessor mSourceEventProcessor = new SourceEventProcessor();
private Listener<float[]> mResampledBufferListener;
private Listener<DecoderStateEvent> mDecoderStateEventListener;
private double mChannelBandwidth;
private double mOutputSampleRate = DEMODULATED_AUDIO_SAMPLE_RATE;
protected boolean mSquelch = true;
/**
* Constructs an instance
*
* @param config to setup the decoder
* @param squelchingDemodulator implementation (e.g. AM or FM).
*/
public SquelchingAnalogDecoder(DecodeConfigAnalog config, ISquelchingDemodulator squelchingDemodulator)
{
super(config);
mDemodulator = squelchingDemodulator;
mChannelBandwidth = config.getBandwidth().getValue();
mDemodulator.setSquelchThreshold(config.getSquelchThreshold());
}
/**
* Implements the ISourceEventListener interface
*/
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceEventProcessor;
}
/**
* Module interface methods - unused.
*/
@Override
public void reset()
{
}
@Override
public void start()
{
}
@Override
public void stop()
{
}
/**
* Broadcasts the demodulated audio samples to the registered listener.
* @param demodulatedSamples to broadcast
*/
protected void broadcast(float[] demodulatedSamples)
{
if(mResampledBufferListener != null)
{
mResampledBufferListener.receive(demodulatedSamples);
}
}
/**
* Implements the IRealBufferProvider interface to register a listener for demodulated audio samples.
*
* @param listener to receive demodulated audio sample buffers.
*/
@Override
public void setBufferListener(Listener<float[]> listener)
{
mResampledBufferListener = listener;
}
/**
* Implements the IRealBufferProvider interface to deregister a listener from receiving demodulated audio samples.
*/
@Override
public void removeBufferListener()
{
mResampledBufferListener = null;
}
/**
* Implements the IComplexSampleListener interface to receive a stream of complex sample buffers.
*/
@Override
public Listener<ComplexSamples> getComplexSamplesListener()
{
return this;
}
/**
* Implements the Listener<ComplexSample> interface to receive a stream of complex sample buffers
*/
@Override
public void receive(ComplexSamples samples)
{
if(mIDecimationFilter == null || mQDecimationFilter == null)
{
throw new IllegalStateException("NBFM demodulator module must receive a sample rate change source " +
"event before it can process complex sample buffers");
}
float[] decimatedI = mIDecimationFilter.decimateReal(samples.i());
float[] decimatedQ = mQDecimationFilter.decimateReal(samples.q());
float[] filteredI = mIBasebandFilter.filter(decimatedI);
float[] filteredQ = mQBasebandFilter.filter(decimatedQ);
float[] demodulated = mDemodulator.demodulate(filteredI, filteredQ);
if(mResampler != null)
{
//Squelch changed while processing this audio buffer
if(mDemodulator.isSquelchChanged())
{
if(mDemodulator.isMuted())
{
//Demodulator says to mute and we're already muted/squelched = Continue IDLE
if(mSquelch)
{
notifyIdle();
}
//Demodulator says to mute and we are unmuted = Squelch and End Call
else
{
mSquelch = true;
notifyCallEnd();
}
}
else
{
//Demodulator says to unmute and we're muted/squelched = Unmute and Start Call
if(mSquelch)
{
mSquelch = false;
notifyCallStart();
}
//Demodulator says to unmute and we're already unmuted = Continue Call
else
{
notifyCallContinuation();
}
mResampler.resample(demodulated);
}
}
else
{
//Demodulator says squelch state didn't change and we're muted/squelched = Continue IDLE
if(mSquelch)
{
notifyIdle();
}
//Demodulator says squelch state didn't change and we're unmuted/unsquelched = Continue CALL
else
{
notifyCallContinuation();
mResampler.resample(demodulated);
}
}
}
else
{
//This shouldn't happen
notifyIdle();
}
}
/**
* Broadcasts a call start state event
*/
protected void notifyCallStart()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.START, State.CALL, 0));
}
/**
* Broadcasts a call continuation state event
*/
private void notifyCallContinuation()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.CALL, 0));
}
/**
* Broadcasts a call end state event
*/
private void notifyCallEnd()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.END, State.CALL, 0));
}
/**
* Broadcasts an idle notification
*/
private void notifyIdle()
{
broadcast(new DecoderStateEvent(this, DecoderStateEvent.Event.CONTINUATION, State.IDLE, 0));
}
/**
* Broadcasts the decoder state event to an optional registered listener
*/
private void broadcast(DecoderStateEvent event)
{
if(mDecoderStateEventListener != null)
{
mDecoderStateEventListener.receive(event);
}
}
/**
* Sets the decoder state listener
*/
@Override
public void setDecoderStateListener(Listener<DecoderStateEvent> listener)
{
mDecoderStateEventListener = listener;
}
/**
* Removes the decoder state event listener
*/
@Override
public void removeDecoderStateListener()
{
mDecoderStateEventListener = null;
}
/**
* Registers the listener to receive squelch change events from the demodulator/squelch controller.
*/
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mDemodulator.setSourceEventListener(listener);
}
/**
* De-registers the listener
*/
@Override
public void removeSourceEventListener()
{
mDemodulator.setSourceEventListener(null);
}
/**
* Monitors sample rate change source event(s) to setup the initial I/Q filter and passes squelch threshold
* change requests down to the demodulator.
*/
public class SourceEventProcessor implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
//Forward these requests to the sub-class implementation (AM or FM demod) to handle
case REQUEST_CURRENT_SQUELCH_THRESHOLD:
case REQUEST_CHANGE_SQUELCH_THRESHOLD:
case REQUEST_CURRENT_SQUELCH_AUTO_TRACK:
case REQUEST_CHANGE_SQUELCH_AUTO_TRACK:
mDemodulator.receive(sourceEvent);
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
if(mIBasebandFilter != null)
{
mIBasebandFilter = null;
mQBasebandFilter = null;
}
double sampleRate = sourceEvent.getValue().doubleValue();
int decimationRate = 0;
double decimatedSampleRate = sampleRate;
if(sampleRate / 2 >= (mChannelBandwidth * 2))
{
decimationRate = 2;
while(sampleRate / decimationRate / 2 >= (mChannelBandwidth * 2))
{
decimationRate *= 2;
}
}
if(decimationRate > 0)
{
decimatedSampleRate /= decimationRate;
}
mIDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
mQDecimationFilter = DecimationFilterFactory.getRealDecimationFilter(decimationRate);
if((decimatedSampleRate < (2.0 * mChannelBandwidth)))
{
throw new IllegalStateException(getDecoderType().name() + " demodulator with channel bandwidth [" +
mChannelBandwidth + "] requires a channel sample rate of [" + (2.0 * mChannelBandwidth +
"] - sample rate of [" + decimatedSampleRate + "] is not supported"));
}
mDemodulator.setSampleRate((int) decimatedSampleRate);
int passBandStop = (int) (mChannelBandwidth * .8);
int stopBandStart = (int) mChannelBandwidth;
float[] coefficients = null;
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(decimatedSampleRate * 2)
.gridDensity(16)
.oddLength(true)
.passBandCutoff(passBandStop)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart(stopBandStart)
.stopBandAmplitude(0.0)
.stopBandRipple(0.005) //Approximately 90 dB attenuation
.build();
try
{
coefficients = FilterFactory.getTaps(specification);
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design demodulator remez filter for sample rate [" + sampleRate +
"] pass frequency [" + passBandStop + "] and stop frequency [" + stopBandStart +
"] - will proceed using sinc (low-pass) filter");
}
if(coefficients == null)
{
mLog.info("Unable to use remez filter designer for sample rate [" + decimatedSampleRate +
"] pass band stop [" + passBandStop +
"] and stop band start [" + stopBandStart + "] - will proceed using simple low pass filter design");
coefficients = FilterFactory.getLowPass(decimatedSampleRate, passBandStop, stopBandStart, 60,
WindowType.HAMMING, true);
}
mIBasebandFilter = FilterFactory.getRealFilter(coefficients);
mQBasebandFilter = FilterFactory.getRealFilter(coefficients);
mResampler = new RealResampler(decimatedSampleRate, mOutputSampleRate, 4192, 512);
mResampler.setListener(resampled -> broadcast(resampled));
break;
}
}
}
}

View File

@@ -44,7 +44,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.DataMessage;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityMaxAloha;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSystemStatus;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataChannelGrant;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Aloha;
@@ -64,6 +64,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasComp
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
@@ -673,10 +674,10 @@ public class DMRDecoderState extends TimeslotDecoderState
updateRestChannel(((CapacityPlusNeighbors)csbk).getRestChannel());
}
break;
case MOTOROLA_CAPPLUS_SYSTEM_STATUS:
if(csbk instanceof CapacityPlusSystemStatus)
case MOTOROLA_CAPPLUS_SITE_STATUS:
if(csbk instanceof CapacityPlusSiteStatus)
{
CapacityPlusSystemStatus cpss = (CapacityPlusSystemStatus)csbk;
CapacityPlusSiteStatus cpss = (CapacityPlusSiteStatus)csbk;
//Channel rotation monitor normally uses only CONTROL state, so when we detect that we're a
//Capacity plus system, add ACTIVE as an active state to the monitor. This can be requested repeatedly.
@@ -1048,13 +1049,27 @@ public class DMRDecoderState extends TimeslotDecoderState
updateRestChannel(((CapacityPlusRestChannel)message).getRestChannel());
}
break;
case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER:
if(message instanceof CapacityPlusGroupVoiceChannelUser)
case FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER:
if(message instanceof CapacityPlusEncryptedVoiceChannelUser cpgvcu)
{
CapacityPlusGroupVoiceChannelUser cpgvcu = (CapacityPlusGroupVoiceChannelUser)message;
//This is the current channel - what do we do with the voice channel number?
// updateRestChannel(cpgvcu.getVoiceChannel());
if(isTerminator)
{
getIdentifierCollection().remove(Role.FROM);
getIdentifierCollection().update(cpgvcu.getTalkgroup());
}
else
{
getIdentifierCollection().update(message.getIdentifiers());
ServiceOptions serviceOptions = cpgvcu.getServiceOptions();
updateCurrentCall(serviceOptions.isEncrypted() ? DecodeEventType.CALL_GROUP_ENCRYPTED :
DecodeEventType.CALL_GROUP, serviceOptions.toString(), message.getTimestamp());
}
}
break;
case FULL_CAPACITY_PLUS_GROUP_VOICE_CHANNEL_USER:
if(message instanceof CapacityPlusGroupVoiceChannelUser cpgvcu)
{
updateRestChannel(cpgvcu.getRestChannel());
if(isTerminator)
{

View File

@@ -377,7 +377,7 @@ public class DMRMessageFramer implements Listener<Dibit>, IDMRBurstDetectListene
public static void main(String[] args)
{
String path = "/media/denny/Lexar/Recordings/DMR/";
String path = "/media/denny/T7 Shield/Recordings/DMR/";
//Con+ Traffic + Voice
// String file = path + "20200513_143340_9600BPS_DMR_SaiaNet_Onondaga_Control.bits"; //Enh GPS Revert Window Annce
@@ -385,7 +385,7 @@ public class DMRMessageFramer implements Listener<Dibit>, IDMRBurstDetectListene
// String file = path + "20200514_063507_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 2579
// String file = path + "20200514_064224_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Window Grant 5056035
// String file = path + "20200514_131623_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //GPS Grant: 5074193
String file = path + "20200514_133947_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits"; //<<<<<<<------ Basic Encryption
// String file = path + "SaiaNet/20200514_133947_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits"; //<<<<<<<------ Basic Encryption
// String file = path + "20200514_142249_9600BPS_DMR_SaiaNet_Onondaga_LCN_4.bits";
// String file = path + "20200514_144534_9600BPS_DMR_SaiaNet_Onondaga_LCN_3_Control.bits"; //Con+ Control w/GPS Window Announce
@@ -395,12 +395,16 @@ public class DMRMessageFramer implements Listener<Dibit>, IDMRBurstDetectListene
//Cap-Max Tier III CC
// String file = path + "20200710_053632_9600BPS_DMR_Niles_Radio_Coconino_Control.bits";
//Cap+ Multi-Site 1 - Traffic LCN 2
// String file = path + "20200716_210133_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
// String file = path + "20200716_212309_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
//Cap+ Multi-Site 1 - Data Revert LSN1/2 with CSBKO 62 Site status
String file = path + "Texas_Dallas_Aerowave_Capacity_Plus_Multi_Site/20200716_210133_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
// String file = path + "Texas_Dallas_Aerowave_Capacity_Plus_Multi_Site/20200716_210845_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
// String file = path + "Texas_Dallas_Aerowave_Capacity_Plus_Multi_Site/20200716_212309_9600BPS_DMR_Aerowave_Technologies_Dallas_LCN_2.bits";
//Cap+ Multi-Site Enhanced GPS Channel
// String file = path + "20200714_224018_9600BPS_DMR_Farmers_Electric_Cooperative_Hunt_LCN_3.bits"; //This may have PLL mis-align issues
//Cap+ Multi-Site Enhanced Data Revert Channel (no CSBKO 62)
// String file = path + "Texas_Hunt_Farmers_Electric_Capacity_Plus_Multi_Site/20200714_224018_9600BPS_DMR_Farmers_Electric_Cooperative_Hunt_LCN_3.bits"; //This may have PLL mis-align issues
//Cap+ Multi-Site - Dallas Unknown
// String file = path + "Texas_Dallas_Unknown_1_Capacity_Plus_Multi_Site/20200716_211233_9600BPS_DMR_Dallas_Unk_Dallas_Unk.bits";
//Hytera Short Data = Proprietary (encrypted)
// String file = path + "20200716_222839_9600BPS_DMR_SystemUnk_SiteUnk_Unk.bits"; //Hytera short data packets

View File

@@ -111,6 +111,9 @@ public class DMRMessageProcessor implements Listener<IMessage>
}
}
//Now that the message has been (potentially) enriched, dispatch it to the modules
dispatch(message);
//Extract the Full Link Control message fragment from the Voice with embedded signalling message
if(message instanceof VoiceEMBMessage)
{
@@ -202,9 +205,6 @@ public class DMRMessageProcessor implements Listener<IMessage>
{
dispatch(mTalkerAliasAssembler.process(flc));
}
//Now that the message has been (potentially) enriched, dispatch it to the modules
dispatch(message);
}
/**

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.channel;
@@ -87,7 +84,7 @@ public class DMRLogicalChannel extends DMRChannel
{
StringBuilder sb = new StringBuilder();
sb.append("LSN:").append(getLogicalSlotNumber());
sb.append(" LCN:").append(getValue());
// sb.append(" LCN:").append(getValue());
return sb.toString();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,6 +28,9 @@ import io.github.dsheirer.module.decode.dmr.message.DMRBurst;
*/
public abstract class DataMessage extends DMRBurst
{
//R2, R1, R0 extracted from the BPTC extraction process. Note: message length remains at 96, even though these 3x bits
//are set at the end of the message.
private static final int[] BPTC_RESERVED_BITS = new int[]{96, 97, 98};
private SlotType mSlotType;
/**
@@ -54,4 +57,24 @@ public abstract class DataMessage extends DMRBurst
{
return mSlotType;
}
/**
* 3x reserved bits that are left-over from the BPTC encode/decode process that can be used to hold values like
* Moto RAS indicator. The message length from the BPTC decoder is set to 96 and these 3x bits are appended to
* the end as overage. However, to keep the message.toString() correct, we specify the length as 96.
* @return reserved value.
*/
public int getBPTCReservedBits()
{
return getMessage().getInt(BPTC_RESERVED_BITS);
}
/**
* Indicates if the BPTC reserved bits value is anything other than 0.
* @return true if non-zero.
*/
public boolean hasRAS()
{
return getBPTCReservedBits() != 0;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,7 +26,6 @@ import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory;
import io.github.dsheirer.module.decode.dmr.message.type.DataType;
import java.util.Collections;
import java.util.List;
@@ -71,6 +70,10 @@ public abstract class DataMessageWithLinkControl extends DataMessage
public String toString()
{
StringBuilder sb = new StringBuilder();
if(hasRAS())
{
sb.append("RAS:").append(getBPTCReservedBits()).append(" ");
}
sb.append(getSlotType());
sb.append(" ").append(getLCMessage());
return sb.toString();

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2020 Zhenyu Mao
* *****************************************************************************
* 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.module.decode.dmr.message.data;
@@ -23,7 +22,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import java.util.Collections;
import java.util.List;
@@ -50,6 +48,12 @@ public class IDLEMessage extends DataMessage
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" IDLE");
return sb.toString();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import java.util.Collections;
import java.util.List;
@@ -61,6 +60,11 @@ public class UnknownDataMessage extends DataMessage
sb.append("TS").append(getTimeslot());
}
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" ").append(getSlotType());
sb.append(" ").append(getMessage().toHexString());

View File

@@ -75,6 +75,11 @@ public abstract class DataBlock extends DataMessage
{
StringBuilder sb = new StringBuilder();
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" ").append(getSlotType());
sb.append(" ").append(getMessage().toHexString());

View File

@@ -39,7 +39,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityP
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusDataRevertWindowGrant;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusNeighbors;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusPreamble;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSystemStatus;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.CapacityPlusSiteStatus;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusCSBKO_16;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataChannelGrant;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.motorola.ConnectPlusDataRevertWindowAnnouncement;
@@ -268,8 +268,8 @@ public class CSBKMessageFactory
case MOTOROLA_CAPPLUS_PREAMBLE:
csbk = new CapacityPlusPreamble(pattern, message, cach, slotType, timestamp, timeslot);
break;
case MOTOROLA_CAPPLUS_SYSTEM_STATUS:
csbk = new CapacityPlusSystemStatus(pattern, message, cach, slotType, timestamp, timeslot);
case MOTOROLA_CAPPLUS_SITE_STATUS:
csbk = new CapacityPlusSiteStatus(pattern, message, cach, slotType, timestamp, timeslot);
break;
case MOTOROLA_CAPPLUS_DATA_WINDOW_ANNOUNCEMENT:
csbk = new CapacityPlusDataRevertWindowAnnouncement(pattern, message, cach, slotType, timestamp, timeslot);

View File

@@ -87,7 +87,7 @@ public enum Opcode
MOTOROLA_CAPPLUS_NEIGHBOR_REPORT(Vendor.MOTOROLA_CAPACITY_PLUS, 59, "NEIGHBOR REPORT"),
MOTOROLA_CAPPLUS_CSBKO_60(Vendor.MOTOROLA_CAPACITY_PLUS, 60, "CSBKO 60"),
MOTOROLA_CAPPLUS_PREAMBLE(Vendor.MOTOROLA_CAPACITY_PLUS, 61, "PREAMBLE"),
MOTOROLA_CAPPLUS_SYSTEM_STATUS(Vendor.MOTOROLA_CAPACITY_PLUS, 62, "SYSTEM STATUS"),
MOTOROLA_CAPPLUS_SITE_STATUS(Vendor.MOTOROLA_CAPACITY_PLUS, 62, "SITE STATUS"),
HYTERA_08_ACKNOWLEDGE(Vendor.HYTERA_8, 32, "HYTERA 08 ACKNOWLEDGE"),
HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "HYTERA 08 ANNOUNCEMENT"),

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.type.Vendor;
import java.util.Collections;
import java.util.List;
@@ -60,6 +59,10 @@ public class UnknownCSBKMessage extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" CSBK *UNKNOWN*");
Vendor vendor = getVendor();

View File

@@ -83,6 +83,10 @@ public class Hytera08Acknowledge extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA 08 ").append(getAcknowledgeType());
sb.append(" REASON:");

View File

@@ -82,6 +82,10 @@ public class Hytera68Acknowledge extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA 68 ").append(getAcknowledgeType());
sb.append(" REASON:");

View File

@@ -85,6 +85,10 @@ public class HyteraAloha extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA ALOHA");
if(hasRadioIdentifier())

View File

@@ -77,6 +77,10 @@ public class HyteraAnnouncement extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA ANNOUNCEMENT-TYPE:").append(getAnnouncementType());
sb.append(" ").append(getSystemIdentityCode().getModel());
sb.append(" NETWORK:").append(getSystemIdentityCode().getNetwork());

View File

@@ -67,6 +67,10 @@ public class HyteraCsbko44 extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA UNKNOWN CSBKO=44");
sb.append(" MSG:").append(getMessage().toHexString());

View File

@@ -75,6 +75,10 @@ public class HyteraCsbko47 extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA CSBKO=47 ######### UNKNOWN");
sb.append(" FM:").append(getSourceRadio());
sb.append(" TO:").append(getDestinationRadio());

View File

@@ -75,6 +75,10 @@ public class HyteraSmsAvailableNotification extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA SMS MESSAGE AVAILABLE FOR:").append(getDestinationRadio());
sb.append(" FM:").append(getSourceRadio());
sb.append(" UNK:").append(getUnknown());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessage;
import java.util.ArrayList;
import java.util.List;
@@ -234,6 +233,10 @@ public class HyteraXPTSiteState extends CSBKMessage
}
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
sb.append(" HYTERA XPT SITE ");
if(isAllChannelsBusy())

Some files were not shown because too many files have changed in this diff Show More