mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 15:47:25 -05:00
Compare commits
15 Commits
v0.5.3
...
v0.6.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b6377cc11 | ||
|
|
9502ca3069 | ||
|
|
77629000b2 | ||
|
|
ca7916f992 | ||
|
|
9de83843a3 | ||
|
|
cbab7bb0c9 | ||
|
|
3df8d60eed | ||
|
|
b39dd1a19f | ||
|
|
0af5963d08 | ||
|
|
4f8b8e978b | ||
|
|
5654254cbb | ||
|
|
11f37e896a | ||
|
|
e8f563deeb | ||
|
|
f877354a11 | ||
|
|
df3d3b5ddc |
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
src/main/java/io/github/dsheirer/dsp/fm/IDemodulator.java
Normal file
34
src/main/java/io/github/dsheirer/dsp/fm/IDemodulator.java
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
139
src/main/java/io/github/dsheirer/log/LoggingSuppressor.java
Normal file
139
src/main/java/io/github/dsheirer/log/LoggingSuppressor.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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:");
|
||||
|
||||
|
||||
@@ -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:");
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user