mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
Compare commits
73 Commits
v0.6.1-bet
...
nightly
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0a0ad4b43 | ||
|
|
17cdd0df27 | ||
|
|
6d23aaa06f | ||
|
|
933629e30d | ||
|
|
8a6bc03ffc | ||
|
|
0c616d4531 | ||
|
|
b472c68b80 | ||
|
|
f6f4b07623 | ||
|
|
de100b4662 | ||
|
|
bbde3033b7 | ||
|
|
f6866f5408 | ||
|
|
d09fad56db | ||
|
|
7c06075880 | ||
|
|
5b597e29eb | ||
|
|
feb66831d4 | ||
|
|
37d47f4219 | ||
|
|
0d886c272e | ||
|
|
48202da9e4 | ||
|
|
c4d9d58a5d | ||
|
|
fe5679fed7 | ||
|
|
b2be38b239 | ||
|
|
912925ee7d | ||
|
|
d86f8bf3d9 | ||
|
|
88d8633958 | ||
|
|
3bf3de82fc | ||
|
|
03a015f504 | ||
|
|
15b7923a4f | ||
|
|
189173e289 | ||
|
|
2dcd994eb2 | ||
|
|
afadb227db | ||
|
|
5dbcd5a8ad | ||
|
|
728a85c180 | ||
|
|
dd6ddd59e9 | ||
|
|
758794fdb4 | ||
|
|
49b95a9445 | ||
|
|
a4bcdfa769 | ||
|
|
872498badc | ||
|
|
4afb74a847 | ||
|
|
96f0b7dd55 | ||
|
|
b360950b80 | ||
|
|
5b0a1401a1 | ||
|
|
3fa114501b | ||
|
|
8935835921 | ||
|
|
dec255d444 | ||
|
|
1cb0490985 | ||
|
|
c8235ea4fc | ||
|
|
7d69d829b5 | ||
|
|
a46abf7da8 | ||
|
|
437264e272 | ||
|
|
8718d04ef5 | ||
|
|
4e367bb5fe | ||
|
|
71d2cb6fca | ||
|
|
e856de273d | ||
|
|
ea0a9da24c | ||
|
|
b3cd7d787a | ||
|
|
9a13a82ed1 | ||
|
|
9b71508d49 | ||
|
|
694ea83a80 | ||
|
|
70fb74b787 | ||
|
|
9ffa02e2e5 | ||
|
|
11abb87177 | ||
|
|
49cf92c7ab | ||
|
|
4cf51405ea | ||
|
|
db83d5121c | ||
|
|
d61d9c07b0 | ||
|
|
5c9c3fa503 | ||
|
|
552d3a6ce9 | ||
|
|
3bf425f94a | ||
|
|
660a5acd2f | ||
|
|
551277f399 | ||
|
|
a12040be5c | ||
|
|
e4be7dc5f4 | ||
|
|
b615c1e16a |
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support and Questions - How do I XXX? Does it support XXX?
|
||||
url: https://github.com/DSheirer/sdrtrunk/wiki/Support
|
||||
about: Please ask your user questions here -- DO NOT create an issue if you have a user question please!!
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
name: How-To Question or Does The Software Support xxx
|
||||
about: End user question or support discussion
|
||||
title: STOP - Don't Use Issue Reporting to Post End-User Questions!!!
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
STOP !!!!
|
||||
|
||||
GitHub issues are for reporting software bugs or requesting enhancements.
|
||||
|
||||
If you have a question about how to use the software, or if you want to know if sdrtrunk supports a protocol, tuner, or other aspect, please post your question in one of the Support forums.
|
||||
|
||||
Visit: https://github.com/DSheirer/sdrtrunk/wiki
|
||||
4
.github/workflows/gradle.yml
vendored
4
.github/workflows/gradle.yml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 23
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '23'
|
||||
java-version: '25'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
|
||||
4
.github/workflows/nightly.yml
vendored
4
.github/workflows/nightly.yml
vendored
@@ -15,10 +15,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 23
|
||||
- name: Set up JDK 25
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '23'
|
||||
java-version: '25'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
|
||||
|
||||
6
.idea/dictionaries/denny.xml
generated
6
.idea/dictionaries/denny.xml
generated
@@ -2,18 +2,24 @@
|
||||
<dictionary name="denny">
|
||||
<words>
|
||||
<w>bahroo</w>
|
||||
<w>baseband</w>
|
||||
<w>basebanded</w>
|
||||
<w>broadcastify</w>
|
||||
<w>cach</w>
|
||||
<w>correlator</w>
|
||||
<w>deinterleave</w>
|
||||
<w>descrambled</w>
|
||||
<w>dibit</w>
|
||||
<w>dinf</w>
|
||||
<w>dqpsk</w>
|
||||
<w>esnid</w>
|
||||
<w>fleetsync</w>
|
||||
<w>heterdyned</w>
|
||||
<w>heterodynes</w>
|
||||
<w>hytera</w>
|
||||
<w>icecast</w>
|
||||
<w>ident</w>
|
||||
<w>lcns</w>
|
||||
<w>nutall</w>
|
||||
<w>overflowable</w>
|
||||
<w>polyphase</w>
|
||||
|
||||
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_23_PREVIEW" project-jdk-name="liberica-23" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25_PREVIEW" project-jdk-name="liberica-25" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
20
README.md
20
README.md
@@ -1,6 +1,24 @@
|
||||

|
||||

|
||||
|
||||
# MacOS Tahoe 26.1 Users - Attention:
|
||||
Changes to USB support in Tahoe version 26.x cause sdrtrunk to fail to launch. Do the following to install the latest libusb and create a symbolic link and then use the nightly build which includes an updated usb4java native library for Tahoe with ARM processor. There may still be issue(s) with MacOS accessing your USB SDR tuners.
|
||||
|
||||
```
|
||||
brew install libusb --HEAD
|
||||
cd /opt
|
||||
sudo mkdir local
|
||||
cd local
|
||||
sudo mkdir lib
|
||||
```
|
||||
Next, find where brew installed the libusb library, for example: ```/opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib``` Note: the folder "HEAD-9ceaa52" is the version stamp for HEAD when you installed from it.
|
||||
|
||||
Finally, create a symbolic link from the installed library to the place where usb4java is expecting to find libusb (/opt/local/lib/libusb-1.0.0.dylib)
|
||||
|
||||
```
|
||||
sudo ln -s /opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib /opt/local/lib/libusb-1.0.0.dylib
|
||||
```
|
||||
|
||||
# sdrtrunk
|
||||
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
|
||||
|
||||
@@ -28,6 +46,6 @@ playlist(s) before you use the nightly builds.** Note: the nightly release is u
|
||||
committed to the code base, so it's not really 'nightly' as much as it is 'current'.
|
||||
|
||||
## Minimum System Requirements
|
||||
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
|
||||
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac (64-bit, 12.x or higher)
|
||||
* **CPU:** 4-core
|
||||
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.
|
||||
|
||||
92
build.gradle
92
build.gradle
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -47,21 +47,21 @@ plugins {
|
||||
id 'application'
|
||||
id 'java'
|
||||
id 'idea'
|
||||
id 'org.beryx.runtime' version '1.12.7'
|
||||
id 'org.beryx.runtime' version '2.0.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
maven { url = "https://jitpack.io" }
|
||||
}
|
||||
|
||||
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
|
||||
version = project.findProperty('projectVersion')
|
||||
|
||||
//Java 23 is required for this version of the Project Panama preview/incubator feature
|
||||
//Java 24 is required for this version of the Project Panama preview/incubator feature
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(23)
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +77,11 @@ test {
|
||||
dependencies {
|
||||
|
||||
// JUnit Tests
|
||||
testImplementation(platform("org.junit:junit-bom:5.11.0"))
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0'
|
||||
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
|
||||
testImplementation(platform("org.junit:junit-bom:6.0.1"))
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.1'
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1")
|
||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
|
||||
//Jitpack imports
|
||||
implementation 'com.github.dnault:libresample4j:master'
|
||||
@@ -89,37 +89,37 @@ dependencies {
|
||||
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||
|
||||
//mavenCentral/jcenter imports
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.12'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.14'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
|
||||
implementation 'com.fazecast:jSerialComm:2.9.3'
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.21'
|
||||
implementation 'ch.qos.logback:logback-core:1.5.21'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.20.1'
|
||||
implementation 'com.fazecast:jSerialComm:2.11.4'
|
||||
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
||||
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
implementation 'com.github.wendykierp:JTransforms:3.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.google.guava:guava:33.3.0-jre'
|
||||
implementation 'com.google.code.gson:gson:2.13.2'
|
||||
implementation 'com.google.guava:guava:33.5.0-jre'
|
||||
implementation 'com.jidesoft:jide-oss:3.6.18'
|
||||
implementation 'com.miglayout:miglayout-swing:11.0'
|
||||
implementation 'com.miglayout:miglayout-swing:11.4.2'
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'commons-io:commons-io:2.21.0'
|
||||
implementation 'eu.hansolo:charts:1.0.5'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
|
||||
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.3' //OSX & Windows aarch64 native libs
|
||||
implementation 'io.github.dsheirer:usb4java:1.3.5' //Fork with Loader support for OSX Multi-Version
|
||||
implementation 'javax.usb:usb-api:1.0.2'
|
||||
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
|
||||
implementation 'org.apache.commons:commons-compress:1.26.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.apache.commons:commons-compress:1.28.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.19.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'org.apache.commons:commons-csv:1.9.0'
|
||||
implementation 'org.apache.mina:mina-core:2.2.1'
|
||||
implementation 'org.apache.mina:mina-http:2.2.1'
|
||||
implementation 'org.controlsfx:controlsfx:11.1.2'
|
||||
implementation 'org.apache.commons:commons-csv:1.14.1'
|
||||
implementation 'org.apache.mina:mina-core:2.2.4'
|
||||
implementation 'org.apache.mina:mina-http:2.2.4'
|
||||
implementation 'org.controlsfx:controlsfx:11.2.2'
|
||||
implementation 'org.rauschig:jarchivelib:1.2.0'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.5'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java-javax:1.3.0'
|
||||
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
|
||||
implementation 'pl.edu.icm:JLargeArrays:1.6'
|
||||
}
|
||||
|
||||
@@ -142,6 +142,9 @@ def jvmArgsWindows =
|
||||
'--add-modules=jdk.incubator.vector', //Project Panama Vector API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector API is still in incubator
|
||||
'--enable-native-access=ALL-UNNAMED', //Suppress unsafe warnings for FFM API Foreign Memory calls
|
||||
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
|
||||
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
|
||||
'-XX:+UseCompactObjectHeaders', //JDK25 - use compact object headers
|
||||
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //SDRPlay API library path
|
||||
|
||||
/**
|
||||
@@ -151,10 +154,13 @@ def jvmArgsLinux =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
|
||||
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
|
||||
'-XX:+UseCompactObjectHeaders', //JDK25 - use compact object headers
|
||||
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
|
||||
'--enable-native-access=ALL-UNNAMED'] //SDRPlay API library path
|
||||
|
||||
application {
|
||||
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
|
||||
mainClass = "io.github.dsheirer.gui.SDRTrunk"
|
||||
|
||||
if(os.isWindows()) {
|
||||
applicationDefaultJvmArgs = jvmArgsWindows
|
||||
@@ -164,6 +170,15 @@ application {
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
if(os.isWindows()) {
|
||||
jvmArgs = jvmArgsWindows
|
||||
}
|
||||
else {
|
||||
jvmArgs = jvmArgsLinux
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
manifest {
|
||||
attributes (
|
||||
@@ -184,6 +199,11 @@ idea {
|
||||
}
|
||||
}
|
||||
|
||||
javadoc {
|
||||
options.addStringOption('-add-modules', 'jdk.incubator.vector')
|
||||
options.addStringOption('Xdoclint:none', '-quiet')
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime target OS and CPU image names. We append version here so that version details are at end of zip filename.
|
||||
*/
|
||||
@@ -199,17 +219,17 @@ String targetWindowsAarch64 = 'windows-aarch64-v' + version
|
||||
* author's development environment, but can be changed to your environment if you want to use this build pattern.
|
||||
*/
|
||||
String jdk_base = '/home/denny/java_jdks/'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-22.0.2-full'
|
||||
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-22.0.2-full'
|
||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-22.0.2-full.jdk'
|
||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-22.0.2-full.jdk'
|
||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-22.0.2-full'
|
||||
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-22.0.2-full'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-25.0.1-full'
|
||||
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-25.0.1-full'
|
||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-25.0.1-full.jdk'
|
||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-25.0.1-full.jdk'
|
||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-25.0.1-full'
|
||||
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-25.0.1-full'
|
||||
|
||||
/**
|
||||
* Download URLs to download the JDK as part of the gradle build packaging process
|
||||
*/
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/23.0.1+13/bellsoft-jdk23.0.1+13-"
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/25.0.1+11/bellsoft-jdk25.0.1+11-"
|
||||
def jdk_download_suffix = "-full.tar.gz"
|
||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
||||
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
||||
@@ -369,5 +389,5 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
}
|
||||
}
|
||||
|
||||
configure(rt, jvmArgsLinux);
|
||||
configure(rt, jvmArgsLinux)
|
||||
}
|
||||
|
||||
@@ -28,4 +28,4 @@
|
||||
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
||||
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
||||
|
||||
projectVersion=0.6.1-beta-3
|
||||
projectVersion=0.6.2-beta-1
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
# Copyright (C) 2014-2025 Dennis Sheirer
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
25
gradlew
vendored
25
gradlew
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
# Copyright (C) 2014-2025 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
|
||||
@@ -58,7 +58,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -86,7 +86,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -114,7 +115,7 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -147,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -155,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -204,16 +205,16 @@ fi
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
26
gradlew.bat
vendored
26
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.alias;
|
||||
|
||||
@@ -30,13 +27,22 @@ import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -46,13 +52,6 @@ import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Alias provides an aliasing (e.g. name, color, etc) container that is linked to multiple alias identifiers and
|
||||
* provides a corresponding set of actions related to the alias.
|
||||
@@ -74,6 +73,7 @@ public class Alias
|
||||
private StringProperty mName = new SimpleStringProperty();
|
||||
private ObservableList<AliasID> mAliasIDs = FXCollections.observableArrayList();
|
||||
private ObservableList<AliasAction> mAliasActions = FXCollections.observableArrayList();
|
||||
private ObjectProperty<StreamAsTalkgroup> mStreamTalkgroupAlias = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance and sets the specified name.
|
||||
@@ -180,6 +180,12 @@ public class Alias
|
||||
return mColor;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public ObjectProperty streamTalkgroupAliasProperty()
|
||||
{
|
||||
return mStreamTalkgroupAlias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon name property
|
||||
*/
|
||||
@@ -229,6 +235,24 @@ public class Alias
|
||||
mName.set(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias name
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "stream_talkgroup_alias")
|
||||
public StreamAsTalkgroup getStreamTalkgroupAlias()
|
||||
{
|
||||
return mStreamTalkgroupAlias.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the stream as talkgroup value that will be used in the TO field for streamed audio calls.
|
||||
* @param streamTalkgroupAlias with a talkgroup value.
|
||||
*/
|
||||
public void setStreamTalkgroupAlias(StreamAsTalkgroup streamTalkgroupAlias)
|
||||
{
|
||||
mStreamTalkgroupAlias.set(streamTalkgroupAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias list name for the alias list that this alias belongs to
|
||||
*/
|
||||
@@ -710,6 +734,7 @@ public class Alias
|
||||
{
|
||||
return (Alias a) -> new Observable[] {a.recordableProperty(), a.streamableProperty(), a.colorProperty(),
|
||||
a.aliasListNameProperty(), a.groupProperty(), a.iconNameProperty(), a.nameProperty(), a.aliasIds(),
|
||||
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty()};
|
||||
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty(), a.priorityProperty(),
|
||||
a.streamTalkgroupAliasProperty()};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -42,6 +42,7 @@ import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -70,6 +71,7 @@ import javafx.util.Callback;
|
||||
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
||||
@JsonSubTypes.Type(value = Record.class, name = "record"),
|
||||
@JsonSubTypes.Type(value = SiteID.class, name = "siteID"),
|
||||
@JsonSubTypes.Type(value = StreamAsTalkgroup.class, name = "streamAsTalkgroup"),
|
||||
@JsonSubTypes.Type(value = Talkgroup.class, name = "talkgroup"),
|
||||
@JsonSubTypes.Type(value = TalkgroupRange.class, name = "talkgroupRange"),
|
||||
@JsonSubTypes.Type(value = TonesID.class, name = "tones"),
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.alias.id.talkgroup;
|
||||
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Streaming talkgroup alias. For streamed audio metadata, this value is used to replace the decoded talkgroup or TO
|
||||
* value with an alias value when there are talkgroup collisions resulting from streaming multiple channels to the
|
||||
* same stream. This is common in P25 conventional repeater setups where each radio channel is using talkgroup 1.
|
||||
*/
|
||||
public class StreamAsTalkgroup extends Talkgroup
|
||||
{
|
||||
public StreamAsTalkgroup(int talkgroup)
|
||||
{
|
||||
super(Protocol.UNKNOWN, talkgroup);
|
||||
}
|
||||
|
||||
public StreamAsTalkgroup()
|
||||
{
|
||||
//No arg JAXB constructor
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,6 +20,8 @@
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
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;
|
||||
@@ -32,6 +34,7 @@ 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.configuration.AliasListConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
@@ -45,6 +48,7 @@ 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;
|
||||
@@ -76,6 +80,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
final private AliasModel mAliasModel;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
@@ -86,6 +91,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
{
|
||||
super(config);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -443,10 +449,34 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
private String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
//Alias the TO value when the user specifies a 'Stream As Talkgroup'
|
||||
if(identifier != null)
|
||||
{
|
||||
AliasListConfigurationIdentifier config = audioRecording.getIdentifierCollection().getAliasListConfiguration();
|
||||
|
||||
if(config != null)
|
||||
{
|
||||
AliasList aliasList = mAliasModel.getAliasList(config.getValue());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
|
||||
for(Alias a: aliases)
|
||||
{
|
||||
if(a.getStreamTalkgroupAlias() != null)
|
||||
{
|
||||
return String.valueOf(a.getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
return format(patchGroupIdentifier);
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.slf4j.LoggerFactory;
|
||||
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
|
||||
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; // 5 seconds
|
||||
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
|
||||
private static final String HTTP_1_0_OK_HEX_DUMP = "48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B";
|
||||
|
||||
@@ -61,6 +62,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
private AtomicBoolean mConnectionFailureLogged = new AtomicBoolean(false);
|
||||
|
||||
/**
|
||||
* Creates an Icecast 2.4.x compatible broadcaster using HTTP 1.1 protocol. This broadcaster is
|
||||
@@ -141,7 +143,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
if(mSocketConnector == null)
|
||||
{
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(10000);
|
||||
mSocketConnector.setConnectTimeoutMillis(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(1000);
|
||||
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastHTTPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageReceivedLogLevel(LogLevel.DEBUG);
|
||||
@@ -160,37 +163,58 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
{
|
||||
try {
|
||||
ConnectFuture future = mSocketConnector
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
future.awaitUninterruptibly();
|
||||
mStreamingSession = future.getSession();
|
||||
}
|
||||
catch(RuntimeIoException rie)
|
||||
{
|
||||
|
||||
boolean connected;
|
||||
try {
|
||||
connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (mConnectionFailureLogged.compareAndSet(false, true)) {
|
||||
mLog.debug("HTTP connect attempt interrupted");
|
||||
}
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
future.cancel();
|
||||
disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
if (connected && future.isConnected()) {
|
||||
mStreamingSession = future.getSession();
|
||||
mConnecting.set(false);
|
||||
mConnectionFailureLogged.set(false);
|
||||
return;
|
||||
} else {
|
||||
if (mConnectionFailureLogged.compareAndSet(false, true)) {
|
||||
mLog.debug("HTTP connect attempt timed out ({} ms) or not connected",
|
||||
CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
|
||||
}
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
future.cancel();
|
||||
disconnect();
|
||||
}
|
||||
} catch (RuntimeIoException rie) {
|
||||
Throwable throwableCause = rie.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
}
|
||||
else if(throwableCause != null)
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLog.debug("Failed to connect", rie);
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
mLog.debug("Failed to connect - no exception is available");
|
||||
}
|
||||
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
if (mConnectionFailureLogged.compareAndSet(false, true)) {
|
||||
if (throwableCause != null) {
|
||||
mLog.debug("Failed to connect", rie);
|
||||
} else {
|
||||
mLog.debug("Failed to connect - no exception is available");
|
||||
}
|
||||
}
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
disconnect();
|
||||
} finally {
|
||||
if (!connected()) {
|
||||
mConnecting.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
mConnecting.set(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -210,6 +234,11 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
if(!getBroadcastState().isErrorState()) {
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,6 +317,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
@@ -323,6 +353,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnectionFailureLogged.set(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -405,6 +436,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
case SUCCESS_OK:
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnecting.set(false);
|
||||
mConnectionFailureLogged.set(false);
|
||||
break;
|
||||
case CLIENT_ERROR_UNAUTHORIZED:
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
@@ -25,21 +22,16 @@ 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.icecast.IcecastConfiguration;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.List;
|
||||
|
||||
public class IcecastMetadata
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadata.class);
|
||||
@@ -73,13 +65,23 @@ public class IcecastMetadata
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
|
||||
List<Alias> aliases = aliasList.getAliases(to);
|
||||
|
||||
if(aliases != null && !aliases.isEmpty())
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -94,7 +94,6 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
if(mInlineActive)
|
||||
{
|
||||
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
|
||||
if (mInlineRemaining == -1)
|
||||
{
|
||||
mInlineRemaining = mInlineInterval;
|
||||
@@ -105,16 +104,34 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
byte[] chunk = Arrays.copyOfRange(audio, audioOffset, Math.min(audioOffset + mInlineRemaining, audio.length));
|
||||
mInlineRemaining -= chunk.length;
|
||||
audioOffset += chunk.length;
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Broadcasting audio data - sending [" + chunk.length + "/" + audio.length + "] bytes");
|
||||
}
|
||||
|
||||
mStreamingSession.write(chunk);
|
||||
if(mInlineRemaining == 0)
|
||||
{
|
||||
mInlineRemaining = mInlineInterval;
|
||||
|
||||
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Sending inline metadata: " + new String(metadata));
|
||||
}
|
||||
|
||||
mStreamingSession.write(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Broadcasting audio data [" + audio.length + "] bytes");
|
||||
}
|
||||
mStreamingSession.write(audio);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@
|
||||
API_KEY("api_key"),
|
||||
SOURCE_LIST("source_list"),
|
||||
FREQ_LIST("freq_list"),
|
||||
PATCHES("patches");
|
||||
PATCH_LIST("patch_list"),
|
||||
TALKER_ALIAS("talker_alias");
|
||||
|
||||
private String mHeader;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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,55 +19,45 @@
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
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.ConnectException;
|
||||
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;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
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.Role;
|
||||
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
|
||||
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 java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
|
||||
@@ -249,6 +239,7 @@
|
||||
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||
String talkgroup = getTo(audioRecording);
|
||||
String radioId = getFrom(audioRecording);
|
||||
String talkerAlias = getTalkerAlias(audioRecording);
|
||||
Long frequency = getFrequency(audioRecording);
|
||||
String patches = getPatches(audioRecording);
|
||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||
@@ -282,8 +273,8 @@
|
||||
.addPart(FormField.TALKGROUP_NUM, talkgroup)
|
||||
.addPart(FormField.EMERGENCY, 0)
|
||||
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||
// For future use if OpenMHz supports patch information
|
||||
//.addPart(FormField.PATCHES, patches)
|
||||
.addPart(FormField.PATCH_LIST, patches)
|
||||
.addPart(FormField.TALKER_ALIAS, talkerAlias)
|
||||
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
|
||||
|
||||
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||
@@ -426,22 +417,40 @@
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
private String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
if(identifier != null)
|
||||
{
|
||||
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();
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
@@ -547,6 +556,28 @@
|
||||
return "[]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with talkerAlias identifier
|
||||
*/
|
||||
private static String getTalkerAlias(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof TalkerAliasIdentifier)
|
||||
{
|
||||
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
|
||||
|
||||
if(talkerID.isValid())
|
||||
{
|
||||
return talkerID.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests both the connection and configuration against the OpenMHz Call API service
|
||||
* @param configuration containing API key and system id
|
||||
|
||||
@@ -36,6 +36,7 @@ public enum FormField
|
||||
SOURCES("sources"),
|
||||
SYSTEM("system"),
|
||||
SYSTEM_LABEL("systemLabel"),
|
||||
TALKER_ALIAS("talkerAlias"),
|
||||
TALKGROUP_ID("talkgroup"),
|
||||
TALKGROUP_GROUP("talkgroupGroup"),
|
||||
TALKGROUP_LABEL("talkgroupLabel"),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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,7 @@ 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.alias.TalkerAliasIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
@@ -48,6 +49,7 @@ import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
@@ -227,6 +229,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||
String talkgroup = getTo(audioRecording);
|
||||
String radioId = getFrom(audioRecording);
|
||||
String talkerAlias = getTalkerAlias(audioRecording);
|
||||
Long frequency = getFrequency(audioRecording);
|
||||
String patches = getPatches(audioRecording);
|
||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||
@@ -261,6 +264,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
.addPart(FormField.TALKGROUP_ID, talkgroup)
|
||||
.addPart(FormField.SOURCE, radioId)
|
||||
.addPart(FormField.FREQUENCY, frequency)
|
||||
.addPart(FormField.TALKER_ALIAS, talkerAlias)
|
||||
.addPart(FormField.TALKGROUP_LABEL, talkgroupLabel)
|
||||
.addPart(FormField.TALKGROUP_GROUP, talkgroupGroup)
|
||||
.addPart(FormField.SYSTEM_LABEL, systemLabel)
|
||||
@@ -408,26 +412,62 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
return "0";
|
||||
}
|
||||
|
||||
private static String getTalkerAlias(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof TalkerAliasIdentifier)
|
||||
{
|
||||
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
|
||||
|
||||
if(talkerID.isValid())
|
||||
{
|
||||
return talkerID.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
private String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
if(identifier != null)
|
||||
{
|
||||
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();
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -29,6 +29,17 @@ import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
@@ -41,17 +52,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
|
||||
@@ -212,10 +212,20 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
|
||||
List<Alias> aliases = aliasList.getAliases(to);
|
||||
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
}
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -49,6 +49,15 @@ import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
@@ -59,15 +68,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class);
|
||||
@@ -303,13 +303,23 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sbTitle2.append("TO:").append(to);
|
||||
|
||||
List<Alias> aliases = aliasList.getAliases(to);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
|
||||
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
|
||||
|
||||
if(streamAs.isPresent())
|
||||
{
|
||||
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
sbTitle2.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
|
||||
}
|
||||
else
|
||||
{
|
||||
sbTitle2.append("TO:").append(to);
|
||||
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -36,7 +36,7 @@ import java.util.List;
|
||||
* radio identifiers.
|
||||
*/
|
||||
@JsonRootName("mbe_call")
|
||||
@JsonPropertyOrder({"protocol", "call_type", "from", "to", "encrypted", "frames"})
|
||||
@JsonPropertyOrder({"protocol", "version", "call_type", "from", "to", "encrypted", "system", "site", "frames"})
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class MBECallSequence
|
||||
{
|
||||
@@ -77,6 +77,15 @@ public class MBECallSequence
|
||||
mProtocol = protocol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version number for the mbe file structure.
|
||||
* Should be incremented only on breaking structural changes, not minor additions.
|
||||
*/
|
||||
@JsonProperty("version")
|
||||
public Integer getVersion()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this sequences contains any audio frames
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -282,7 +282,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
return;
|
||||
}
|
||||
|
||||
if(buffer != null)
|
||||
if(buffer != null && buffer.array().length > 0)
|
||||
{
|
||||
int wrote = 0;
|
||||
|
||||
@@ -303,102 +303,22 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
|
||||
checkStart();
|
||||
|
||||
//Something is causing the source data line to fail to accept audio byte data via the write() method
|
||||
//and this seems to have started around JDK22, maybe. We can detect when this happens and close() and
|
||||
//reopen the data line to clear the error.
|
||||
if(!mOutput.isRunning() && wrote <= 0 && buffer.array().length > 0)
|
||||
//Something is causing the source data line to fail to accept audio byte data via the write() method
|
||||
//and this seems to have started around JDK22, maybe. When this happens, close and then re-open the
|
||||
//data line to clear the error state. Note: in testing this error condition, the line is showing
|
||||
//the buffer is empty and the capacity is fully available, so it should have accepted attempts to write
|
||||
//data, but it failed, as indicated by wrote=0.
|
||||
if(!mOutput.isRunning() && wrote <= 0)
|
||||
{
|
||||
mOutput.close();
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = (SourceDataLine)mMixer.getLine(mLineInfo);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
mGainControl = (FloatControl) gain;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
|
||||
"MASTER GAIN control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
|
||||
"MASTER MUTE control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
}
|
||||
|
||||
LOGGING_SUPPRESSOR.info("reopen audio output success", 2,
|
||||
"Closed and reopened audio output - success - mOutput is not null");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGING_SUPPRESSOR.info("reopen audio output fail", 2,
|
||||
"Closed and reopened audio output - fail - mOutput is null");
|
||||
return;
|
||||
}
|
||||
mOutput.open();
|
||||
}
|
||||
catch(LineUnavailableException lue)
|
||||
catch(Exception e)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("reopen fail for lua", 2, "Attempt to reopen " +
|
||||
"audio source data line failed", lue);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mOutput.open(mAudioFormat, mRequestedBufferSize);
|
||||
|
||||
//Start threshold: buffer is full with 10% or less of capacity remaining
|
||||
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
|
||||
|
||||
//Stop threshold: buffer is empty with 90% or more capacity available
|
||||
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
|
||||
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
mGainControl = (FloatControl) gain;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" + getChannelName() +
|
||||
"] after reopening the source data line");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" + getChannelName() +
|
||||
"] after reopening the source data line");
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 2,
|
||||
"Failed to (re)open the source data line for audio output - mixer [" +
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
return;
|
||||
mLog.error("Error after closing and attempting to reopen audio output", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -65,6 +66,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledExecutorService mScheduledExecutorService =
|
||||
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
|
||||
private AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
|
||||
private ReentrantLock mAudioOutputLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
@@ -95,6 +98,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
mLog.warn("No audio output devices available");
|
||||
}
|
||||
|
||||
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,36 +195,53 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
}
|
||||
else if(audioSegment.isLinked())
|
||||
{
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
mAudioOutputLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
it.remove();
|
||||
audioOutput.play(audioSegment);
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
{
|
||||
it.remove();
|
||||
audioOutput.play(audioSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Sort audio segments by playback priority and assign to empty audio outputs
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
//TODO: change this to take audio segment start time into account also
|
||||
mAudioSegments.sort(Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
|
||||
mAudioSegments.sort(mAudioSegmentPrioritySorter);
|
||||
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
mAudioOutputLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(audioOutput.isEmpty())
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
if(audioOutput.isEmpty())
|
||||
{
|
||||
return;
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
//Remove any audio segments marked as complete that didn't get assigned to an output
|
||||
@@ -288,37 +311,39 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
||||
|
||||
if(mProcessingTask != null)
|
||||
mAudioOutputLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mProcessingTask.cancel(true);
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputs.clear();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||
mAudioOutputs.add(mono);
|
||||
break;
|
||||
case STEREO:
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
||||
mAudioOutputs.add(left);
|
||||
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
||||
mAudioOutputs.add(right);
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
}
|
||||
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputs.clear();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||
mAudioOutputs.add(mono);
|
||||
break;
|
||||
case STEREO:
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
||||
mAudioOutputs.add(left);
|
||||
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
||||
mAudioOutputs.add(right);
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||
}
|
||||
|
||||
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
||||
mMixerChannelConfiguration = entry;
|
||||
}
|
||||
@@ -389,4 +414,30 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio segment comparator for sorting audio segments by: 1)Playback priority and 2)Segment start time
|
||||
*/
|
||||
public class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
|
||||
{
|
||||
@Override
|
||||
public int compare(AudioSegment segment1, AudioSegment segment2)
|
||||
{
|
||||
if(segment1 == null || segment2 == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
//If priority is the same, sort by start time
|
||||
if(segment1.monitorPriorityProperty().get() == segment2.monitorPriorityProperty().get())
|
||||
{
|
||||
return Long.compare(segment1.getStartTimestamp(), segment2.getStartTimestamp());
|
||||
}
|
||||
//Otherwise, sort by priority
|
||||
else
|
||||
{
|
||||
return Integer.compare(segment1.monitorPriorityProperty().get(), segment2.monitorPriorityProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -243,6 +243,16 @@ public class BinaryMessage extends BitSet
|
||||
return mSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current size of the message indicating the quantity of bits that have been added to this message. This value
|
||||
* is less than or equal to the size() value.
|
||||
* @return current size in bits.
|
||||
*/
|
||||
public int currentSize()
|
||||
{
|
||||
return mPointer;
|
||||
}
|
||||
|
||||
public void setSize(int size)
|
||||
{
|
||||
mSize = size;
|
||||
@@ -281,6 +291,20 @@ public class BinaryMessage extends BitSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds bit1 and bit2 to this bitset.
|
||||
*
|
||||
* Note: this method does not provide overfill protection.
|
||||
*
|
||||
* @param bit1 to add.
|
||||
* @param bit2 to add.
|
||||
*/
|
||||
public void add(boolean bit1, boolean bit2)
|
||||
{
|
||||
set(mPointer++, bit1);
|
||||
set(mPointer++, bit2);
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -401,8 +425,8 @@ public class BinaryMessage extends BitSet
|
||||
boolean[] returnValue = null;
|
||||
|
||||
if(startIndex >= 0 &&
|
||||
startIndex < endIndex &&
|
||||
endIndex < mSize)
|
||||
startIndex < endIndex &&
|
||||
endIndex < mSize)
|
||||
{
|
||||
returnValue = new boolean[endIndex - startIndex + 1];
|
||||
|
||||
@@ -433,7 +457,7 @@ public class BinaryMessage extends BitSet
|
||||
if(bits.length > 32)
|
||||
{
|
||||
throw new IllegalArgumentException("Overflow - must be 32 bits "
|
||||
+ "or less to fit into a primitive integer value");
|
||||
+ "or less to fit into a primitive integer value");
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
@@ -662,7 +686,7 @@ public class BinaryMessage extends BitSet
|
||||
if(bits.length > 32)
|
||||
{
|
||||
throw new IllegalArgumentException("Overflow - must be 32 bits "
|
||||
+ "or less to fit into a primitive integer value");
|
||||
+ "or less to fit into a primitive integer value");
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
@@ -697,6 +721,28 @@ public class BinaryMessage extends BitSet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the integer value to the field described by the argument.
|
||||
* @param value to set
|
||||
* @param intField describing the indices of the field.
|
||||
*/
|
||||
public void setInt(int value, IntField intField)
|
||||
{
|
||||
for(int x = 0; x < intField.width(); x++)
|
||||
{
|
||||
int mask = 1 << (intField.width() - x - 1);
|
||||
|
||||
if((value & mask) == mask)
|
||||
{
|
||||
set(intField.start() + x);
|
||||
}
|
||||
else
|
||||
{
|
||||
clear(intField.start() + x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte value represented by the bit array
|
||||
*
|
||||
@@ -710,7 +756,7 @@ public class BinaryMessage extends BitSet
|
||||
if(bits.length != 8)
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid - there must be 8"
|
||||
+ "indexes to form a proper byte");
|
||||
+ "indexes to form a proper byte");
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
@@ -742,7 +788,7 @@ public class BinaryMessage extends BitSet
|
||||
if(bits.length != 8)
|
||||
{
|
||||
throw new IllegalArgumentException("Invalid - there must be 8"
|
||||
+ "indexes to form a proper byte");
|
||||
+ "indexes to form a proper byte");
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
@@ -870,7 +916,7 @@ public class BinaryMessage extends BitSet
|
||||
if(bits.length > 64)
|
||||
{
|
||||
throw new IllegalArgumentException("Overflow - must be 64 bits "
|
||||
+ "or less to fit into a primitive long value");
|
||||
+ "or less to fit into a primitive long value");
|
||||
}
|
||||
|
||||
long value = 0;
|
||||
@@ -902,7 +948,7 @@ public class BinaryMessage extends BitSet
|
||||
if(bits.length > 64)
|
||||
{
|
||||
throw new IllegalArgumentException("Overflow - must be 64 bits "
|
||||
+ "or less to fit into a primitive long value");
|
||||
+ "or less to fit into a primitive long value");
|
||||
}
|
||||
|
||||
long value = 0;
|
||||
@@ -990,7 +1036,7 @@ public class BinaryMessage extends BitSet
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("BitSetBuffer.getHex() "
|
||||
+ "maximum array length is 63 bits");
|
||||
+ "maximum array length is 63 bits");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1011,7 +1057,7 @@ public class BinaryMessage extends BitSet
|
||||
if(FastMath.abs(end - start) > 32)
|
||||
{
|
||||
throw new IllegalArgumentException("Overflow - must be 32 bits "
|
||||
+ "or less to fit into a primitive integer value");
|
||||
+ "or less to fit into a primitive integer value");
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
@@ -1086,7 +1132,7 @@ public class BinaryMessage extends BitSet
|
||||
if(FastMath.abs(end - start) > 64)
|
||||
{
|
||||
throw new IllegalArgumentException("Overflow - must be 64 bits "
|
||||
+ "or less to fit into a primitive long value");
|
||||
+ "or less to fit into a primitive long value");
|
||||
}
|
||||
|
||||
long value = 0;
|
||||
@@ -1241,7 +1287,7 @@ public class BinaryMessage extends BitSet
|
||||
if(!message.matches("[01]*"))
|
||||
{
|
||||
throw new IllegalArgumentException(
|
||||
"Message must contain only zeros and ones");
|
||||
"Message must contain only zeros and ones");
|
||||
}
|
||||
|
||||
BinaryMessage buffer = new BinaryMessage(message.length());
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -69,6 +69,16 @@ public record IntField(int start, int end)
|
||||
return new IntField(start, (start + 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one four bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length6(int start)
|
||||
{
|
||||
return new IntField(start, (start + 5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one octet of length.
|
||||
* @param start index (inclusive)
|
||||
|
||||
@@ -1,35 +1,31 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Multi-Sync pattern matcher. Accepts multiple sync detector objects for
|
||||
* comparision against the incoming bit stream.
|
||||
@@ -39,6 +35,7 @@ import java.util.List;
|
||||
* of the Long.rotate methods will wrap the MSB around to the LSB and
|
||||
* corrupt the matching value.
|
||||
*/
|
||||
@Deprecated //remove after P25P2 decoder is updated
|
||||
public class MultiSyncPatternMatcher
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class);
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
@Deprecated //Remove after P25P2 decoder is updated.
|
||||
public class SoftSyncDetector implements ISyncProcessor
|
||||
{
|
||||
private ISyncDetectListener mListener;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -29,7 +29,7 @@ import java.util.Iterator;
|
||||
*/
|
||||
public class ByteNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
private static final int FRAGMENT_SIZE = 2048;
|
||||
private static final int FRAGMENT_SIZE = 8192;
|
||||
private final static float[] LOOKUP_VALUES;
|
||||
private float mAverageDc;
|
||||
|
||||
|
||||
@@ -1,57 +1,153 @@
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2014 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-2025 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;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Averaging buffer backed by a ring buffer implemented with a float array.
|
||||
*/
|
||||
public class FloatAveragingBuffer
|
||||
{
|
||||
private float[] mBuffer;
|
||||
private float mAverage = 0.0f;
|
||||
private int mBufferSize;
|
||||
private int mBufferPointer;
|
||||
|
||||
public FloatAveragingBuffer( int size )
|
||||
{
|
||||
mBufferSize = size;
|
||||
mBuffer = new float[ size ];
|
||||
}
|
||||
|
||||
public float get( float newValue )
|
||||
{
|
||||
float oldValue = mBuffer[ mBufferPointer ];
|
||||
private final float[] mBuffer;
|
||||
private final int mBufferSize;
|
||||
private float mAverage = 0.0f;
|
||||
private int mBufferPointer;
|
||||
private boolean mRapidFill = false;
|
||||
private int mRapidFillCounter = 0;
|
||||
private int mRapidFillIncrement;
|
||||
|
||||
if( Float.isInfinite( newValue ) || Float.isNaN( newValue ) )
|
||||
{
|
||||
mAverage = mAverage - ( oldValue / mBufferSize );
|
||||
/**
|
||||
* Constructs an instance that uses a rapid fill increment value where the initial values get counted multiple times
|
||||
* to more quickly get to the average value instead of walking up to the average from the default average value of 0.
|
||||
* @param size of buffer
|
||||
* @param rapidFillIncrement indicating how many value additions per individual add to execute until the averaging
|
||||
* buffer is filled. After the initial fill state is achieved, reverts to using a single value addition.
|
||||
*/
|
||||
public FloatAveragingBuffer(int size, int rapidFillIncrement)
|
||||
{
|
||||
this(size);
|
||||
mRapidFillIncrement = rapidFillIncrement;
|
||||
mRapidFill = true;
|
||||
}
|
||||
|
||||
mBuffer[ mBufferPointer++ ] = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
mAverage = mAverage - ( oldValue / mBufferSize ) + ( newValue / mBufferSize );
|
||||
/**
|
||||
* Constructs an instance that uses a ring buffer of the specified size.
|
||||
* @param size of the averaging buffer.
|
||||
*/
|
||||
public FloatAveragingBuffer(int size)
|
||||
{
|
||||
mBufferSize = size;
|
||||
mBuffer = new float[size];
|
||||
}
|
||||
|
||||
mBuffer[ mBufferPointer++ ] = newValue;
|
||||
}
|
||||
/**
|
||||
* Resets this buffer to an empty state
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
Arrays.fill(mBuffer, 0);
|
||||
mBufferPointer = 0;
|
||||
mAverage = 0.0f;
|
||||
|
||||
if( mBufferPointer >= mBufferSize )
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
if(mRapidFillIncrement > 0)
|
||||
{
|
||||
mRapidFill = true;
|
||||
mRapidFillCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return mAverage;
|
||||
}
|
||||
/**
|
||||
* Current average value.
|
||||
* @return average
|
||||
*/
|
||||
public float getAverage()
|
||||
{
|
||||
return mAverage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the value to the buffer and updates the average.
|
||||
* @param value to add
|
||||
*/
|
||||
public void add(float value)
|
||||
{
|
||||
if(mRapidFill)
|
||||
{
|
||||
for(int x = 0; x < mRapidFillIncrement; x++)
|
||||
{
|
||||
float oldValue = mBuffer[mBufferPointer];
|
||||
|
||||
if(Float.isInfinite(value) || Float.isNaN(value))
|
||||
{
|
||||
mAverage = mAverage - (oldValue / mBufferSize);
|
||||
mBuffer[mBufferPointer++] = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize);
|
||||
mBuffer[mBufferPointer++] = value;
|
||||
}
|
||||
|
||||
if(mBufferPointer >= mBufferSize)
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
mRapidFillCounter += mRapidFillIncrement;
|
||||
|
||||
if(mRapidFillCounter > mBufferSize)
|
||||
{
|
||||
mRapidFill = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float oldValue = mBuffer[mBufferPointer];
|
||||
|
||||
if(Float.isInfinite(value) || Float.isNaN(value))
|
||||
{
|
||||
mAverage = mAverage - (oldValue / mBufferSize);
|
||||
mBuffer[mBufferPointer++] = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize);
|
||||
mBuffer[mBufferPointer++] = value;
|
||||
}
|
||||
|
||||
if(mBufferPointer >= mBufferSize)
|
||||
{
|
||||
mBufferPointer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the value to the buffer and calculates a new average.
|
||||
* @param value to add
|
||||
* @return updated average
|
||||
*/
|
||||
public float get(float value)
|
||||
{
|
||||
add(value);
|
||||
return mAverage;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -30,7 +30,7 @@ import java.util.Iterator;
|
||||
public abstract class AirspyBufferIterator<T> implements Iterator<T>
|
||||
{
|
||||
protected static final float SCALE_SIGNED_12_BIT_TO_FLOAT = 1.0f / 2048.0f;
|
||||
protected static final int FRAGMENT_SIZE = 2048;
|
||||
protected static final int FRAGMENT_SIZE = 4096;
|
||||
protected static final float[] COEFFICIENTS =
|
||||
HilbertTransform.convertHalfBandToHilbert(HilbertTransform.HALF_BAND_FILTER_47_TAP);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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,7 +21,7 @@ package io.github.dsheirer.channel.metadata;
|
||||
import com.jidesoft.swing.JideSplitPane;
|
||||
import com.jidesoft.swing.JideTabbedPane;
|
||||
import io.github.dsheirer.channel.details.ChannelDetailPanel;
|
||||
import io.github.dsheirer.gui.power.ChannelPowerPanel;
|
||||
import io.github.dsheirer.gui.channel.ChannelSpectrumPanel;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
|
||||
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
|
||||
@@ -42,7 +42,7 @@ public class NowPlayingPanel extends JPanel
|
||||
private final ChannelDetailPanel mChannelDetailPanel;
|
||||
private final DecodeEventPanel mDecodeEventPanel;
|
||||
private final MessageActivityPanel mMessageActivityPanel;
|
||||
private final ChannelPowerPanel mChannelPowerPanel;
|
||||
private final ChannelSpectrumPanel mChannelSpectrumSquelchPanel;
|
||||
private JideTabbedPane mTabbedPane;
|
||||
private JideSplitPane mSplitPane;
|
||||
private boolean mDetailTabsVisible;
|
||||
@@ -58,7 +58,7 @@ public class NowPlayingPanel extends JPanel
|
||||
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
||||
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
||||
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
||||
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
||||
mChannelSpectrumSquelchPanel = new ChannelSpectrumPanel(playlistManager, settingsManager);
|
||||
mDetailTabsVisible = detailTabsVisible;
|
||||
|
||||
init();
|
||||
@@ -96,12 +96,12 @@ public class NowPlayingPanel extends JPanel
|
||||
mTabbedPane.addTab("Details", mChannelDetailPanel);
|
||||
mTabbedPane.addTab("Events", mDecodeEventPanel);
|
||||
mTabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
mTabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||
mTabbedPane.addTab("Channel", mChannelSpectrumSquelchPanel);
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
mTabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||
.indexOfComponent(mChannelPowerPanel)));
|
||||
mTabbedPane.addChangeListener(e -> mChannelSpectrumSquelchPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||
.indexOfComponent(mChannelSpectrumSquelchPanel)));
|
||||
}
|
||||
|
||||
return mTabbedPane;
|
||||
@@ -135,6 +135,6 @@ public class NowPlayingPanel extends JPanel
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelSpectrumSquelchPanel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
@@ -56,15 +56,14 @@ import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationType;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Utility methods for designing various types of filters.
|
||||
*/
|
||||
@@ -627,11 +626,13 @@ public class FilterFactory
|
||||
* @param samplesPerSymbol - number of samples per symbol
|
||||
* @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size
|
||||
* @param alpha - roll-off factor.
|
||||
* @param gain of the filter
|
||||
* @return - filter coefficients
|
||||
*/
|
||||
public static float[] getRootRaisedCosine(double samplesPerSymbol, int symbolCount, float alpha)
|
||||
{
|
||||
int taps = (int)(samplesPerSymbol * symbolCount);
|
||||
samplesPerSymbol /= 2;
|
||||
int taps = (int)Math.round(samplesPerSymbol * symbolCount);
|
||||
|
||||
float scale = 0;
|
||||
|
||||
@@ -639,47 +640,47 @@ public class FilterFactory
|
||||
|
||||
for(int x = 0; x < taps; x++)
|
||||
{
|
||||
float index = (float)x - ((float)taps / 2.0f);
|
||||
double index = x - (taps / 2.0f);
|
||||
|
||||
float x1 = (float)FastMath.PI * index / (float)samplesPerSymbol;
|
||||
float x2 = 4.0f * alpha * index / (float)samplesPerSymbol;
|
||||
float x3 = x2 * x2 - 1.0f;
|
||||
double x1 = FastMath.PI * index / samplesPerSymbol;
|
||||
double x2 = 4.0 * alpha * index / samplesPerSymbol;
|
||||
double x3 = x2 * x2 - 1.0;
|
||||
|
||||
float numerator, denominator;
|
||||
double numerator, denominator;
|
||||
|
||||
if(FastMath.abs(x3) >= 0.000001)
|
||||
{
|
||||
if(x != taps / 2)
|
||||
{
|
||||
numerator = (float)FastMath.cos((1.0 + alpha) * x1) +
|
||||
(float)FastMath.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol);
|
||||
numerator = FastMath.cos((1.0 + alpha) * x1) +
|
||||
FastMath.sin((1.0 - alpha) * x1) / (4.0 * alpha * index / samplesPerSymbol);
|
||||
}
|
||||
else
|
||||
{
|
||||
numerator = (float)FastMath.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)FastMath.PI / (4.0f * alpha);
|
||||
numerator = FastMath.cos((1.0 + alpha) * x1) + (1.0 - alpha) * FastMath.PI / (4.0 * alpha);
|
||||
}
|
||||
|
||||
denominator = x3 * (float)FastMath.PI;
|
||||
denominator = x3 * FastMath.PI;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(alpha == 1.0f)
|
||||
if(alpha == 1.0)
|
||||
{
|
||||
coefficients[x] = -1.0f;
|
||||
continue;
|
||||
}
|
||||
|
||||
x3 = (1.0f - alpha) * x1;
|
||||
x2 = (1.0f + alpha) * x1;
|
||||
x3 = (1.0 - alpha) * x1;
|
||||
x2 = (1.0 + alpha) * x1;
|
||||
|
||||
numerator = (float)(FastMath.sin(x2) * (1.0f + alpha) * FastMath.PI - FastMath.cos(x3) * ((1.0 - alpha) * FastMath.PI *
|
||||
(double)samplesPerSymbol) / (4.0 * alpha * index) + FastMath.sin(x3) * (double)samplesPerSymbol *
|
||||
(double)samplesPerSymbol / (4.0 * alpha * index * index));
|
||||
numerator = (FastMath.sin(x2) * (1.0 + alpha) * FastMath.PI - FastMath.cos(x3) * ((1.0 - alpha) * FastMath.PI *
|
||||
samplesPerSymbol) / (4.0 * alpha * index) + FastMath.sin(x3) * samplesPerSymbol *
|
||||
samplesPerSymbol / (4.0 * alpha * index * index));
|
||||
|
||||
denominator = (float)(-32.0 * FastMath.PI * alpha * alpha * index / (double)samplesPerSymbol);
|
||||
denominator = -32.0 * FastMath.PI * alpha * alpha * index / samplesPerSymbol;
|
||||
}
|
||||
|
||||
coefficients[x] = 4.0f * alpha * numerator / denominator;
|
||||
coefficients[x] = (float)(4.0 * alpha * numerator / denominator);
|
||||
|
||||
scale += coefficients[x];
|
||||
}
|
||||
@@ -1210,13 +1211,20 @@ public class FilterFactory
|
||||
{
|
||||
DecimalFormat df = new DecimalFormat("0.000000");
|
||||
|
||||
int length = 15;
|
||||
int length = 49;
|
||||
|
||||
float [] taps = FilterFactory.getHalfBand(length, WindowType.HAMMING);
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
try
|
||||
{
|
||||
mLog.debug("Tap: " + x + " Value: " + df.format(taps[x]));
|
||||
float [] taps = FilterFactory.getSinc(0.5, length, WindowType.NONE);
|
||||
|
||||
for(int x = 0; x < length; x++)
|
||||
{
|
||||
mLog.debug("Tap: " + x + " Value: " + df.format(taps[x]));
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
mLog.debug("Done!");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -302,10 +302,17 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
|
||||
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
case NOTIFICATION_RECORDING_FILE_LOADED:
|
||||
//no-op
|
||||
break;
|
||||
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
|
||||
//Re-broadcast this event to each channel so that the decoders can reset error tracking function(s).
|
||||
List<TunerChannelSource> channels = new ArrayList<>(mChannelSources);
|
||||
for(TunerChannelSource channel: channels)
|
||||
{
|
||||
channel.broadcastConsumerSourceEvent(sourceEvent);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.info("Unrecognized source event: " + sourceEvent);
|
||||
break;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -42,6 +42,7 @@ public class DecimationFilterFactory
|
||||
switch(decimationRate)
|
||||
{
|
||||
case 0:
|
||||
case 1:
|
||||
return new RealDecimateX0Filter();
|
||||
case 2:
|
||||
return new RealDecimateX2Filter();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,7 +20,7 @@
|
||||
package io.github.dsheirer.dsp.filter.design;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
@@ -59,12 +59,32 @@ public class FilterViewer extends Application
|
||||
*/
|
||||
private float[] getFilter()
|
||||
{
|
||||
int filterLength = 47;
|
||||
WindowType windowType = WindowType.HAMMING;
|
||||
int filterLength = 31;
|
||||
WindowType windowType = WindowType.BLACKMAN_HARRIS_7;
|
||||
|
||||
float[] taps = FilterFactory.getHalfBand(filterLength, windowType);
|
||||
FIRFilterSpecification specification = FIRFilterSpecification
|
||||
.lowPassBuilder()
|
||||
.sampleRate(12500)
|
||||
.passBandCutoff(5200)
|
||||
.passBandAmplitude(1.0).passBandRipple(0.01) //.01
|
||||
.stopBandAmplitude(0.0).stopBandStart(6500) //6500
|
||||
.stopBandRipple(0.01).build();
|
||||
|
||||
taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
|
||||
float[] taps = null;
|
||||
|
||||
try
|
||||
{
|
||||
taps = FilterFactory.getTaps(specification);
|
||||
}
|
||||
catch(Exception fde) //FilterDesignException
|
||||
{
|
||||
System.out.println("Error");
|
||||
}
|
||||
|
||||
// float[] taps = FilterFactory.getHighPass(8000, 3000, filterLength, windowType);
|
||||
// float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f);
|
||||
|
||||
// taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
|
||||
|
||||
if(taps == null)
|
||||
{
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.interpolator;
|
||||
|
||||
/*******************************************************************************
|
||||
* SDR Trunk
|
||||
* Copyright (C) 2015 Dennis Sheirer
|
||||
*
|
||||
* Copyright (C) 2002,2012 Free Software Foundation, Inc.
|
||||
* Ported to java from gnuradio - interpolator_taps.h
|
||||
*
|
||||
* 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/>
|
||||
******************************************************************************/
|
||||
public abstract class Interpolator
|
||||
{
|
||||
public static final int NTAPS = 8;
|
||||
@@ -158,4 +157,19 @@ public abstract class Interpolator
|
||||
{ -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128
|
||||
{ 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
|
||||
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
|
||||
* offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
|
||||
* converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
|
||||
* the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
|
||||
* produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
|
||||
*
|
||||
* @param samples - sample array of length at least offset + 7
|
||||
* @param offset into the sample array for the start of the 8-value sequence
|
||||
* @param mu - interpolated sample position between 0.0 and 1.0
|
||||
* @return - interpolated sample value
|
||||
*/
|
||||
public abstract float filter(float[] samples, int offset, float mu);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.interpolator;
|
||||
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationType;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
|
||||
/**
|
||||
* Factory for creating an optimal scalar or vector (SIMD) interpolator implementation
|
||||
*/
|
||||
public class InterpolatorFactory
|
||||
{
|
||||
/**
|
||||
* Selects and instantiates the best interpolator version, scalar or vector, based on previous calibration.
|
||||
* @return interpolator
|
||||
*/
|
||||
public static Interpolator getInterpolator()
|
||||
{
|
||||
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.INTERPOLATOR);
|
||||
return getInterpolator(implementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates the specified interpolator implementation.
|
||||
* @param implementation to construct.
|
||||
* @return interpolator.
|
||||
*/
|
||||
public static Interpolator getInterpolator(Implementation implementation)
|
||||
{
|
||||
return switch(implementation)
|
||||
{
|
||||
case VECTOR_SIMD_64 -> new InterpolatorVector64();
|
||||
case VECTOR_SIMD_128 -> new InterpolatorVector128();
|
||||
case VECTOR_SIMD_256, VECTOR_SIMD_512 -> new InterpolatorVector256(); //Vector 512 not supported
|
||||
case SCALAR, UNCALIBRATED -> new InterpolatorScalar();
|
||||
default -> throw new IllegalArgumentException("Unknown implementation type: " + implementation);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,35 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.interpolator;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class RealInterpolator extends Interpolator
|
||||
public class InterpolatorScalar extends Interpolator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
|
||||
private final static Logger mLog = LoggerFactory.getLogger(InterpolatorScalar.class);
|
||||
|
||||
private float mGain;
|
||||
|
||||
@@ -21,11 +39,16 @@ public class RealInterpolator extends Interpolator
|
||||
*
|
||||
* @param gain to apply to the interpolated sample
|
||||
*/
|
||||
public RealInterpolator(float gain)
|
||||
public InterpolatorScalar(float gain)
|
||||
{
|
||||
mGain = gain;
|
||||
}
|
||||
|
||||
public InterpolatorScalar()
|
||||
{
|
||||
this(1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
|
||||
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
|
||||
@@ -68,7 +91,7 @@ public class RealInterpolator extends Interpolator
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
RealInterpolator interpolator = new RealInterpolator(1.0f);
|
||||
InterpolatorScalar interpolator = new InterpolatorScalar(1.0f);
|
||||
DecimalFormat decimalFormat = new DecimalFormat("0.0000");
|
||||
|
||||
double TWO_PI = FastMath.PI * 2.0;
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.interpolator;
|
||||
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector interpolator for 128 bit SIMD instructions.
|
||||
*/
|
||||
public class InterpolatorVector128 extends VectorInterpolator
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
|
||||
|
||||
@Override
|
||||
protected float vectorFilter(float[] samples, int offset, int index)
|
||||
{
|
||||
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
|
||||
FloatVector tapsVector, sampleVector;
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 4);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 4);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
return accumulator.reduceLanes(VectorOperators.ADD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.interpolator;
|
||||
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector interpolator for 256 bit SIMD instructions.
|
||||
*/
|
||||
public class InterpolatorVector256 extends VectorInterpolator
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
|
||||
|
||||
@Override
|
||||
protected float vectorFilter(float[] samples, int offset, int index)
|
||||
{
|
||||
FloatVector tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
|
||||
FloatVector sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
|
||||
return sampleVector.mul(tapsVector).reduceLanes(VectorOperators.ADD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.interpolator;
|
||||
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector interpolator for 64 bit SIMD instructions.
|
||||
*/
|
||||
public class InterpolatorVector64 extends VectorInterpolator
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
|
||||
|
||||
@Override
|
||||
protected float vectorFilter(float[] samples, int offset, int index)
|
||||
{
|
||||
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
|
||||
FloatVector tapsVector, sampleVector;
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 2);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 2);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 4);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 4);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 6);
|
||||
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 6);
|
||||
accumulator = tapsVector.fma(sampleVector, accumulator);
|
||||
|
||||
return accumulator.reduceLanes(VectorOperators.ADD);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.interpolator;
|
||||
|
||||
/**
|
||||
* Linear interpolator.
|
||||
*/
|
||||
public class LinearInterpolator
|
||||
{
|
||||
/**
|
||||
* Calculates an interpolated value between x1 and x2 at a linear position mu between 0.0 and 1.0
|
||||
* @param x1 first value
|
||||
* @param x2 second value
|
||||
* @param mu offset between first and second values in range 0.0 to 1.0
|
||||
* @return interpolated value.
|
||||
*/
|
||||
public static float calculate(float x1, float x2, float mu)
|
||||
{
|
||||
if(mu < 0)
|
||||
{
|
||||
return x1;
|
||||
}
|
||||
else if(mu > 1)
|
||||
{
|
||||
return x2;
|
||||
}
|
||||
|
||||
return x1 + ((x2 - x1) * mu);
|
||||
}
|
||||
|
||||
public static float calculate(float x1, float x2, double mu)
|
||||
{
|
||||
return calculate(x1, x2, (float)mu);
|
||||
}
|
||||
}
|
||||
@@ -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.filter.interpolator;
|
||||
|
||||
import org.apache.commons.lang3.Validate;
|
||||
|
||||
/**
|
||||
* Base vector interpolator implementation.
|
||||
*/
|
||||
public abstract class VectorInterpolator extends Interpolator
|
||||
{
|
||||
@Override
|
||||
public float filter(float[] samples, int offset, float mu)
|
||||
{
|
||||
/* Ensure we have enough samples in the array */
|
||||
Validate.isTrue(samples.length >= offset + 7, "Offset [" + offset + "] must be 7 less than length[" + samples.length + "]");
|
||||
|
||||
//Identify the filter bank that corresponds to mu. Note: since we're not loading the TAPS in reverse order,
|
||||
//we select the inverse tap index that has the mirrored set of taps, by subtracting from 127.
|
||||
int index = 127 - (int)(NSTEPS * mu);
|
||||
return vectorFilter(samples, offset, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform vectorized interpolation to find the interpolated value from the samples where the 8-value sequence
|
||||
* starts at the offset into the array, using the filter coefficients specified by the index value.
|
||||
* @param samples to interpolate
|
||||
* @param offset to the start of the 8-value sequence
|
||||
* @param index into the TAPS coefficients array.
|
||||
* @return interpolated value.
|
||||
*/
|
||||
protected abstract float vectorFilter(float[] samples, int offset, int index);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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,10 +19,6 @@
|
||||
|
||||
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;
|
||||
|
||||
@@ -32,63 +28,22 @@ import org.slf4j.LoggerFactory;
|
||||
public abstract class VectorFMDemodulator implements IDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
private static final int BUFFER_OVERLAP = 1;
|
||||
|
||||
//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];
|
||||
private ScalarFMDemodulator mScalar;
|
||||
|
||||
public VectorFMDemodulator()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector species as specified by the implementing sub-class.
|
||||
* Access the scalar implementation when the sample buffer size is not a multiple of the vector size.
|
||||
*/
|
||||
protected abstract VectorSpecies<Float> getVectorSpecies();
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
protected ScalarFMDemodulator getScalarImplementation()
|
||||
{
|
||||
// VectorSpecies<Float> species= getVectorSpecies();
|
||||
|
||||
VectorUtilities.checkComplexArrayLength(i, q, getVectorSpecies());
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
if(mScalar == null)
|
||||
{
|
||||
mIBuffer = new float[i.length + BUFFER_OVERLAP];
|
||||
mQBuffer = new float[q.length + BUFFER_OVERLAP];
|
||||
mScalar = new ScalarFMDemodulator();
|
||||
}
|
||||
|
||||
//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 += getVectorSpecies().length())
|
||||
{
|
||||
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()));
|
||||
|
||||
//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;
|
||||
return mScalar;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator128 implements IDemodulator
|
||||
public class VectorFMDemodulator128 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator128.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator128 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i, q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator256 implements IDemodulator
|
||||
public class VectorFMDemodulator256 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator256.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator256 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator512 implements IDemodulator
|
||||
public class VectorFMDemodulator512 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator512.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator512 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
package io.github.dsheirer.dsp.fm;
|
||||
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
|
||||
*/
|
||||
public class VectorFMDemodulator64 implements IDemodulator
|
||||
public class VectorFMDemodulator64 extends VectorFMDemodulator
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator64.class);
|
||||
protected static final float ZERO = 0.0f;
|
||||
@@ -44,9 +43,14 @@ public class VectorFMDemodulator64 implements IDemodulator
|
||||
{
|
||||
}
|
||||
|
||||
@Override public float[] demodulate(float[] i, float[] q)
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
|
||||
{
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator<Dibit>
|
||||
{
|
||||
protected DQPSKDecisionDirectedSymbolEvaluator mSymbolEvaluator = new DQPSKDecisionDirectedSymbolEvaluator();
|
||||
private Complex mPreviousPrecedingSample = new Complex(0, 0);
|
||||
private Complex mPreviousCurrentSample = new Complex(0, 0);
|
||||
private Complex mPrecedingSample = new Complex(0, 0);
|
||||
private Complex mCurrentSample = new Complex(0, 0);
|
||||
private Complex mPrecedingSymbol = new Complex(0, 0);
|
||||
protected Complex mCurrentSymbol = new Complex(0, 0);
|
||||
|
||||
/**
|
||||
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
|
||||
* a decision-directed DQPSK symbol timing and phase error detector to automatically align to the incoming carrier
|
||||
* frequency and to adjust for any changes in symbol timing.
|
||||
*
|
||||
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
|
||||
*
|
||||
* @param phaseLockedLoop for tracking carrier frequency error
|
||||
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
|
||||
*/
|
||||
public DQPSKDecisionDirectedDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
|
||||
{
|
||||
super(interpolatingSampleBuffer, phaseLockedLoop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a symbol from the interpolating buffer
|
||||
*/
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//Get preceding sample and an interpolated current sample from the interpolating buffer
|
||||
mPrecedingSample = getInterpolatingSampleBuffer().getPrecedingSample();
|
||||
mCurrentSample = getInterpolatingSampleBuffer().getCurrentSample();
|
||||
|
||||
//Differential decode preceding and current symbols by calculating the angular rotation between the previous and
|
||||
//current samples (current sample x complex conjugate of previous sample).
|
||||
|
||||
//Note: preceding symbol is a preceding measurement of the current symbol that is simply used as a reference
|
||||
//point to determine vector rotation to the current symbol -- it is not the true predecessor symbol
|
||||
mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
|
||||
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
|
||||
mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
|
||||
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
|
||||
|
||||
mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
|
||||
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
|
||||
mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
|
||||
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
|
||||
|
||||
//Set gain to unity before we calculate the error value
|
||||
mPrecedingSymbol.normalize();
|
||||
mCurrentSymbol.normalize();
|
||||
|
||||
//Apply symbols to evaluator to calculate phase and timing error and make a symbol decision
|
||||
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
|
||||
|
||||
//Update the symbol timing error
|
||||
getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError());
|
||||
|
||||
//Update the costas loop (PLL) with any measured phase error
|
||||
getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
|
||||
|
||||
//Store current samples/symbols to use for the next symbol period
|
||||
mPreviousPrecedingSample.setValues(mPrecedingSample);
|
||||
mPreviousCurrentSample.setValues(mCurrentSample);
|
||||
|
||||
broadcast(mSymbolEvaluator.getSymbolDecision());
|
||||
}
|
||||
}
|
||||
@@ -1,147 +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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class DQPSKDecisionDirectedDemodulatorInstrumented extends DQPSKDecisionDirectedDemodulator
|
||||
{
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<ComplexSamples> mFilteredGainAppliedComplexBufferListener;
|
||||
private double mSampleRate;
|
||||
|
||||
/**
|
||||
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
|
||||
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
|
||||
* adjust for any changes in symbol timing.
|
||||
*
|
||||
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
|
||||
*
|
||||
* @param phaseLockedLoop for tracking carrier frequency error
|
||||
* @param interpolatingSampleBuffer
|
||||
* @param sampleRate of the incoming complex sample stream
|
||||
*/
|
||||
public DQPSKDecisionDirectedDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
|
||||
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
|
||||
double sampleRate)
|
||||
{
|
||||
super(phaseLockedLoop, interpolatingSampleBuffer);
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexSamples complexSamples)
|
||||
{
|
||||
if(mFilteredGainAppliedComplexBufferListener != null)
|
||||
{
|
||||
mFilteredGainAppliedComplexBufferListener.receive(complexSamples);
|
||||
}
|
||||
|
||||
super.receive(complexSamples);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent class symbol calculation to capture eye diagram data
|
||||
*/
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//Eye diagram listener
|
||||
if(mSymbolDecisionDataListener != null)
|
||||
{
|
||||
mSymbolDecisionDataListener.receive(
|
||||
((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData());
|
||||
}
|
||||
|
||||
super.calculateSymbol();
|
||||
|
||||
if(mSamplesPerSymbolListener != null)
|
||||
{
|
||||
mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
|
||||
}
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mComplexSymbolListener != null)
|
||||
{
|
||||
mComplexSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
{
|
||||
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
|
||||
}
|
||||
|
||||
if(mPLLFrequencyListener != null)
|
||||
{
|
||||
double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency();
|
||||
|
||||
loopFrequency *= mSampleRate / (2.0 * FastMath.PI);
|
||||
|
||||
mPLLFrequencyListener.receive(loopFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive symbol decision data to produce an eye diagram.
|
||||
*/
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||
{
|
||||
mSamplesPerSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive decoded QPSK symbols
|
||||
*/
|
||||
public void setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mComplexSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLErrorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator<Dibit>
|
||||
{
|
||||
private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * FastMath.PI / 4.0);
|
||||
private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * FastMath.PI / 4.0);
|
||||
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * FastMath.PI / 4.0);
|
||||
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * FastMath.PI / 4.0);
|
||||
|
||||
private Complex mEvaluationSymbol = new Complex(0, 0);
|
||||
private float mPhaseError = 0.0f;
|
||||
private float mTimingError = 0.0f;
|
||||
private float mTimingErrorPolarity = 1.0f;
|
||||
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
|
||||
|
||||
/**
|
||||
* Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer.
|
||||
*
|
||||
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
|
||||
*
|
||||
* Phase and timing error are calculated as the angular distance of the sampled symbol from the reference
|
||||
* symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
|
||||
* relative to the immediately preceding symbol.
|
||||
*/
|
||||
public DQPSKDecisionDirectedSymbolEvaluator()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the preceding and current symbols to be evaluated for phase and timing errors and to determine the
|
||||
* transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
|
||||
* phase and timing errors and the symbol decision via their respective accessor methods.
|
||||
*
|
||||
* Phase and timing error values are calculated by first determining the symbol and then calculating the phase
|
||||
* and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
|
||||
* relative to the angular vector rotation so that the error value indicates the correct error direction.
|
||||
*
|
||||
* @param preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated
|
||||
* since it's only a reference
|
||||
* @param current symbol to be evaluated - interpolated
|
||||
*/
|
||||
public void setSymbol(Complex preceding, Complex current)
|
||||
{
|
||||
mEvaluationSymbol.setValues(current);
|
||||
|
||||
if(mEvaluationSymbol.quadrature() > 0.0f)
|
||||
{
|
||||
if(mEvaluationSymbol.inphase() > 0.0f)
|
||||
{
|
||||
mSymbolDecision = Dibit.D00_PLUS_1;
|
||||
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
|
||||
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_45);
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolDecision = Dibit.D01_PLUS_3;
|
||||
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
|
||||
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_135);
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mEvaluationSymbol.inphase() > 0.0f)
|
||||
{
|
||||
mSymbolDecision = Dibit.D10_MINUS_1;
|
||||
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
|
||||
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_45);
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolDecision = Dibit.D11_MINUS_3;
|
||||
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
|
||||
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_135);
|
||||
}
|
||||
}
|
||||
|
||||
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
|
||||
//arctan of the error angle relative to 0 radians and this provides our error value
|
||||
float errorNormalized = normalize(mEvaluationSymbol.quadrature(), 0.3f);
|
||||
|
||||
mPhaseError = -errorNormalized;
|
||||
|
||||
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
|
||||
mTimingError = errorNormalized * mTimingErrorPolarity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains value to the range of ( -maximum <> maximum )
|
||||
*/
|
||||
private static float clip(float value, float maximum)
|
||||
{
|
||||
if(value > maximum)
|
||||
{
|
||||
return maximum;
|
||||
}
|
||||
else if(value < -maximum)
|
||||
{
|
||||
return -maximum;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- the maximum value and corrects any
|
||||
* floating point invalid numbers
|
||||
*/
|
||||
private float normalize(float error, float maximum)
|
||||
{
|
||||
if(Float.isNaN(error))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip(error, maximum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase error of the symbol relative to the nearest reference symbol.
|
||||
*
|
||||
* @return phase error in radians of distance from the reference symbol.
|
||||
*/
|
||||
@Override
|
||||
public float getPhaseError()
|
||||
{
|
||||
return mPhaseError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timing error of the symbol relative to the nearest reference symbol.
|
||||
*
|
||||
* @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
|
||||
* originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
|
||||
* if the symbol was sampled early (-) or late (+) relative to the reference symbol.
|
||||
*/
|
||||
@Override
|
||||
public float getTimingError()
|
||||
{
|
||||
return mTimingError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reference symbol that is closest to the transmitted/sampled symbol.
|
||||
*/
|
||||
@Override
|
||||
public Dibit getSymbolDecision()
|
||||
{
|
||||
return mSymbolDecision;
|
||||
}
|
||||
}
|
||||
@@ -1,152 +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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
|
||||
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class DQPSKGardnerDemodulatorInstrumented extends DQPSKGardnerDemodulator
|
||||
{
|
||||
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
|
||||
private Listener<Double> mSamplesPerSymbolListener;
|
||||
private Listener<Complex> mComplexSymbolListener;
|
||||
private Listener<Double> mPLLErrorListener;
|
||||
private Listener<Double> mPLLFrequencyListener;
|
||||
private Listener<ComplexSamples> mFilteredGainAppliedComplexBufferListener;
|
||||
private double mSampleRate;
|
||||
|
||||
/**
|
||||
* Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector.
|
||||
*
|
||||
* This decoder is optimized for P25 Linear Simulcast Modulation (LSM).
|
||||
*
|
||||
* @param phaseLockedLoop for tracking carrier frequency error
|
||||
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
|
||||
*/
|
||||
public DQPSKGardnerDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
|
||||
InterpolatingSampleBuffer interpolatingSampleBuffer,
|
||||
double sampleRate)
|
||||
{
|
||||
super(phaseLockedLoop, interpolatingSampleBuffer);
|
||||
mSampleRate = sampleRate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ComplexSamples complexSamples)
|
||||
{
|
||||
if(mFilteredGainAppliedComplexBufferListener != null)
|
||||
{
|
||||
mFilteredGainAppliedComplexBufferListener.receive(complexSamples);
|
||||
}
|
||||
|
||||
super.receive(complexSamples);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent class symbol calculation to capture eye diagram data
|
||||
*/
|
||||
protected void calculateSymbol()
|
||||
{
|
||||
//Eye diagram listener
|
||||
if(mSymbolDecisionDataListener != null)
|
||||
{
|
||||
mSymbolDecisionDataListener.receive(
|
||||
((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData());
|
||||
}
|
||||
|
||||
super.calculateSymbol();
|
||||
|
||||
if(mSamplesPerSymbolListener != null)
|
||||
{
|
||||
mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
|
||||
}
|
||||
|
||||
//Send to an external constellation symbol listener when registered
|
||||
if(mComplexSymbolListener != null)
|
||||
{
|
||||
mComplexSymbolListener.receive(mCurrentSymbol);
|
||||
}
|
||||
|
||||
if(mPLLErrorListener != null)
|
||||
{
|
||||
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
|
||||
}
|
||||
|
||||
if(mPLLFrequencyListener != null)
|
||||
{
|
||||
double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency();
|
||||
|
||||
loopFrequency *= mSampleRate / (2.0 * FastMath.PI);
|
||||
|
||||
mPLLFrequencyListener.receive(loopFrequency);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive symbol decision data to produce an eye diagram.
|
||||
*/
|
||||
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
|
||||
{
|
||||
mSymbolDecisionDataListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setSamplesPerSymbolListener(Listener<Double> listener)
|
||||
{
|
||||
mSamplesPerSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive decoded QPSK symbols
|
||||
*/
|
||||
public void setComplexSymbolListener(Listener<Complex> listener)
|
||||
{
|
||||
mComplexSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLErrorListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLErrorListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive PLL error values
|
||||
*/
|
||||
public void setPLLFrequencyListener(Listener<Double> listener)
|
||||
{
|
||||
mPLLFrequencyListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive complex sample buffers that have been filtered with automatic gain control applied
|
||||
*/
|
||||
public void setFilteredGainAppliedComplexBufferListener(Listener<ComplexSamples> listener)
|
||||
{
|
||||
mFilteredGainAppliedComplexBufferListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class GardnerDetector
|
||||
{
|
||||
private float mErrorInphase;
|
||||
private float mErrorQuadrature;
|
||||
private float mError;
|
||||
|
||||
/**
|
||||
* Gardner detector for recovering symbol timing error.
|
||||
*/
|
||||
public GardnerDetector()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the symbol timing error by evaluating the previous, middle and current symbol instants.
|
||||
*
|
||||
* @param previous symbol
|
||||
* @param middle symbol (ie midway between the previous and current)
|
||||
* @param current symbol
|
||||
* @return symbol timing error
|
||||
*/
|
||||
public float getError(Complex previous, Complex middle, Complex current)
|
||||
{
|
||||
mErrorInphase = (FastMath.abs(previous.inphase()) - FastMath.abs(current.inphase())) * FastMath.abs(middle.inphase());
|
||||
mErrorQuadrature = (FastMath.abs(previous.quadrature()) - FastMath.abs(current.quadrature())) * FastMath.abs(middle.quadrature());
|
||||
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
|
||||
|
||||
return mError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers
|
||||
*/
|
||||
public static float normalize(float error, float maximum)
|
||||
{
|
||||
if(Float.isNaN(error))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return clip(error, maximum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constrains value to the range of ( -maximum <> maximum )
|
||||
*/
|
||||
public static float clip(float value, float maximum)
|
||||
{
|
||||
if(value > maximum)
|
||||
{
|
||||
return maximum;
|
||||
}
|
||||
else if(value < -maximum)
|
||||
{
|
||||
return -maximum;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public interface IQPSKSymbolDecoder
|
||||
{
|
||||
/**
|
||||
* Decodes the symbol and converts it into a Dibit
|
||||
* @param symbol to decode
|
||||
* @return decoded dibit
|
||||
*/
|
||||
Dibit decode(Complex symbol);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public interface ISymbolPhaseErrorCalculator
|
||||
{
|
||||
/**
|
||||
* Calculates the phase error of the symbol relative to an alignment for the constellation
|
||||
* @param symbol to calculate error
|
||||
* @return error value
|
||||
*/
|
||||
float getPhaseError(Complex symbol);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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,7 +18,7 @@
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
|
||||
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorScalar;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
@@ -45,7 +45,7 @@ public class InterpolatingSampleBuffer
|
||||
private float mMaximumSamplesPerSymbol;
|
||||
private float mMinimumSamplesPerSymbol;
|
||||
|
||||
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
|
||||
private InterpolatorScalar mInterpolator = new InterpolatorScalar(1.0f);
|
||||
|
||||
/**
|
||||
* Buffer to store complex sample data and produce interpolated samples.
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * 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/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSampleListener;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSampleBufferInstrumented.class);
|
||||
private SymbolDecisionData mSymbolDecisionData;
|
||||
private ComplexSampleListener mSampleListener;
|
||||
private int mBufferLength;
|
||||
|
||||
public InterpolatingSampleBufferInstrumented(float samplesPerSymbol, float symbolTimingGain)
|
||||
{
|
||||
super(samplesPerSymbol, symbolTimingGain);
|
||||
mBufferLength = (int) FastMath.ceil(samplesPerSymbol);
|
||||
mSymbolDecisionData = new SymbolDecisionData(mBufferLength);
|
||||
}
|
||||
|
||||
public void receive(Complex sample)
|
||||
{
|
||||
super.receive(sample);
|
||||
mSymbolDecisionData.receive(sample);
|
||||
|
||||
if(mSampleListener != null)
|
||||
{
|
||||
mSampleListener.receive(sample.inphase(), sample.quadrature());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can
|
||||
* be used to support an external eye-diagram chart.
|
||||
* @return symbol decision data.
|
||||
*/
|
||||
public SymbolDecisionData getSymbolDecisionData()
|
||||
{
|
||||
for(int x = mDelayLinePointer; x < mDelayLinePointer + mBufferLength; x++)
|
||||
{
|
||||
mSymbolDecisionData.receive(mDelayLineInphase[x], mDelayLineQuadrature[x]);
|
||||
}
|
||||
|
||||
mSymbolDecisionData.setSamplingPoint(getSamplingPoint());
|
||||
return mSymbolDecisionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the listener to receive samples being sent to this buffer. Note: these samples have
|
||||
* already been corrected by the PLL, so this provides an ideal tap point for PLL corrected samples.
|
||||
* @param listener to receive samples.
|
||||
*/
|
||||
public void setSampleListener(ComplexSampleListener listener)
|
||||
{
|
||||
mSampleListener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class LSMConstellationDecoder implements IQPSKSymbolDecoder
|
||||
{
|
||||
@Override
|
||||
public Dibit decode(Complex symbol)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
|
||||
public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener<Complex>
|
||||
{
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
|
||||
/**
|
||||
* Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
|
||||
*
|
||||
* 00 +3 | 01 +1
|
||||
* ------|------
|
||||
* 10 -1 | 11 -1
|
||||
*/
|
||||
public QPSKStarSlicer()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
mBroadcaster = null;
|
||||
}
|
||||
|
||||
public void addListener(Listener<Dibit> listener)
|
||||
{
|
||||
mBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
public void removeListener(Listener<Dibit> listener)
|
||||
{
|
||||
mBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(Complex sample)
|
||||
{
|
||||
mBroadcaster.broadcast(decode(sample));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dibit decode(Complex sample)
|
||||
{
|
||||
if(sample.inphase() > 0)
|
||||
{
|
||||
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class QPSKSymbolDecoder implements Listener<Complex>, IQPSKSymbolDecoder
|
||||
{
|
||||
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
|
||||
|
||||
/**
|
||||
* Slices a ComplexSample representing a phase shifted symbol according to
|
||||
* the following constellation pattern:
|
||||
*
|
||||
* \ 00 /
|
||||
* \ /
|
||||
* 01 \/ 10
|
||||
* /\
|
||||
* / \
|
||||
* / 11 \
|
||||
*/
|
||||
public QPSKSymbolDecoder()
|
||||
{
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
mBroadcaster.dispose();
|
||||
mBroadcaster = null;
|
||||
}
|
||||
|
||||
public void addListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.addListener( listener );
|
||||
}
|
||||
|
||||
public void removeListener( Listener<Dibit> listener )
|
||||
{
|
||||
mBroadcaster.removeListener( listener );
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive( Complex complex )
|
||||
{
|
||||
mBroadcaster.broadcast( decode( complex ) );
|
||||
}
|
||||
|
||||
public Dibit decode(Complex complex )
|
||||
{
|
||||
if( FastMath.abs( complex.inphase() ) > FastMath.abs( complex.quadrature() ) )
|
||||
{
|
||||
if( complex.inphase() > 0 )
|
||||
{
|
||||
return Dibit.D10_MINUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Dibit.D01_PLUS_3;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if( complex.quadrature() > 0 )
|
||||
{
|
||||
return Dibit.D00_PLUS_1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Dibit.D11_MINUS_3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
/**
|
||||
* Calculates the phase error for a QPSK polar-orientation constellation
|
||||
*/
|
||||
public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator
|
||||
{
|
||||
private float mPhaseError;
|
||||
|
||||
@Override
|
||||
public float getPhaseError(Complex symbol)
|
||||
{
|
||||
mPhaseError = 0.0f;
|
||||
|
||||
if(FastMath.abs(symbol.inphase()) > FastMath.abs(symbol.quadrature()))
|
||||
{
|
||||
if(symbol.inphase() > 0)
|
||||
{
|
||||
mPhaseError = -symbol.quadrature();
|
||||
}
|
||||
else
|
||||
{
|
||||
mPhaseError = symbol.quadrature();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(symbol.quadrature() > 0)
|
||||
{
|
||||
mPhaseError = symbol.inphase();
|
||||
}
|
||||
else
|
||||
{
|
||||
mPhaseError = -symbol.inphase();
|
||||
}
|
||||
}
|
||||
|
||||
return mPhaseError;
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * 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/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk;
|
||||
|
||||
import io.github.dsheirer.buffer.ComplexCircularBuffer;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class SymbolDecisionData
|
||||
{
|
||||
//45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient
|
||||
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(FastMath.PI / 4.0d);
|
||||
|
||||
private ComplexCircularBuffer mBuffer;
|
||||
private float mSamplingPoint;
|
||||
private float mSamplesPerSymbol;
|
||||
|
||||
/**
|
||||
* Circular buffer for capturing two symbols worth of sample data for use in instrumentation.
|
||||
* @param samplesPerSymbol to size the buffer
|
||||
*/
|
||||
public SymbolDecisionData(float samplesPerSymbol)
|
||||
{
|
||||
mSamplesPerSymbol = samplesPerSymbol;
|
||||
mBuffer = new ComplexCircularBuffer(8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the complex sample in the buffer
|
||||
*/
|
||||
public void receive(float inphase, float quadrature)
|
||||
{
|
||||
mBuffer.put(new Complex(inphase, quadrature));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the complex sample in the buffer
|
||||
*/
|
||||
public void receive(Complex complex)
|
||||
{
|
||||
mBuffer.put(complex.copy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the fractional sampling point for the current symbol to use in interpolating the current symbol.
|
||||
*/
|
||||
public void setSamplingPoint(float samplingPoint)
|
||||
{
|
||||
mSamplingPoint = samplingPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fractional sampling point
|
||||
*/
|
||||
public float getSamplingPoint()
|
||||
{
|
||||
return mSamplingPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw samples
|
||||
*/
|
||||
public Complex[] getSamples()
|
||||
{
|
||||
return mBuffer.getAll();
|
||||
}
|
||||
|
||||
public Complex[] getSamples(int length)
|
||||
{
|
||||
return mBuffer.get(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of (FM) demodulated samples representing the current symbol where each current sample is demodulated against
|
||||
* the previous symbol's sample to produce the differential demodulated sample. Samples are in time order.
|
||||
*
|
||||
* Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation.
|
||||
*/
|
||||
public Complex[] getDemodulated()
|
||||
{
|
||||
Complex[] samples = mBuffer.getAll();
|
||||
|
||||
int length = samples.length / 2;
|
||||
|
||||
Complex[] demodulated = new Complex[length];
|
||||
|
||||
for(int x = 0; x < samples.length / 2; x++)
|
||||
{
|
||||
Complex copy = samples[length + x].copy();
|
||||
copy.multiply(samples[x].conjugate());
|
||||
demodulated[x] = copy;
|
||||
}
|
||||
|
||||
return demodulated;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 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.psk;
|
||||
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public enum SymbolInversion
|
||||
{
|
||||
DEGREES_90_PLUS(FastMath.PI / 2.0),
|
||||
DEGREES_90_MINUS(FastMath.PI / -2.0),
|
||||
DEGREES_180(FastMath.PI);
|
||||
|
||||
private double mAngleRadians;
|
||||
|
||||
SymbolInversion(double angleRadians)
|
||||
{
|
||||
mAngleRadians = angleRadians;
|
||||
}
|
||||
|
||||
public double getError()
|
||||
{
|
||||
return mAngleRadians;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.psk.demod;
|
||||
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationType;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
|
||||
/**
|
||||
* Factory for creating PSK differential demodulators
|
||||
*/
|
||||
public class DifferentialDemodulatorFactory
|
||||
{
|
||||
/**
|
||||
* Creates the optimal float output demodulator using calibration data to select the optimal implementation from
|
||||
* scalar and vector options, or defaults to the scalar implementation if the calibration wasn't executed.
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate in Hertz
|
||||
* @return demodulator instance
|
||||
*/
|
||||
public static DifferentialDemodulatorFloat getFloatDemodulator(double sampleRate, int symbolRate)
|
||||
{
|
||||
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.DIFFERENTIAL_DEMODULATOR);
|
||||
|
||||
return switch(implementation)
|
||||
{
|
||||
case VECTOR_SIMD_64 -> new DifferentialDemodulatorFloatVector64(sampleRate, symbolRate);
|
||||
case VECTOR_SIMD_128 -> new DifferentialDemodulatorFloatVector128(sampleRate, symbolRate);
|
||||
case VECTOR_SIMD_256 -> new DifferentialDemodulatorFloatVector256(sampleRate, symbolRate);
|
||||
case VECTOR_SIMD_512 -> new DifferentialDemodulatorFloatVector512(sampleRate, symbolRate);
|
||||
default -> new DifferentialDemodulatorFloatScalar(sampleRate, symbolRate); //Includes SCALAR case
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.psk.demod;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.interpolator.Interpolator;
|
||||
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorFactory;
|
||||
import io.github.dsheirer.dsp.fm.IDemodulator;
|
||||
|
||||
/**
|
||||
* Differential demodulator base class
|
||||
*/
|
||||
public abstract class DifferentialDemodulatorFloat implements IDemodulator
|
||||
{
|
||||
private int mSymbolRate;
|
||||
private float mSampleRate;
|
||||
private float mSamplesPerSymbol;
|
||||
protected float[] mIBuffer = new float[20]; //Initial size 20 for array copy, but gets resized on first buffer
|
||||
protected float[] mQBuffer = new float[20];
|
||||
protected float mMu;
|
||||
protected int mBufferOverlap;
|
||||
protected int mInterpolationOffset;
|
||||
protected final Interpolator mInterpolator = InterpolatorFactory.getInterpolator();
|
||||
private DifferentialDemodulatorFloatScalar mScalar;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DifferentialDemodulatorFloat(double sampleRate, int symbolRate)
|
||||
{
|
||||
mSampleRate = (float) sampleRate;
|
||||
mSymbolRate = symbolRate;
|
||||
mSamplesPerSymbol = mSampleRate / mSymbolRate;
|
||||
mMu = mSamplesPerSymbol % 1; //Fractional part of the samples per symbol rate
|
||||
mInterpolationOffset = (int) Math.floor(mSamplesPerSymbol) - 4; //Interpolate at the middle of 8x samples
|
||||
mBufferOverlap = (int) Math.floor(mSamplesPerSymbol) + 4;
|
||||
|
||||
while(mInterpolationOffset < 0)
|
||||
{
|
||||
mInterpolationOffset++;
|
||||
mBufferOverlap++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scalar implementation for fallback when the sample buffer size is not compatible with the vector implementation.
|
||||
* @return scalar implementation lazy instantiated when needed.
|
||||
*/
|
||||
protected DifferentialDemodulatorFloatScalar getScalarImplementation()
|
||||
{
|
||||
if(mScalar == null)
|
||||
{
|
||||
mScalar = new DifferentialDemodulatorFloatScalar(mSampleRate, mSymbolRate);
|
||||
}
|
||||
|
||||
return mScalar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculated samples per symbol.
|
||||
* @return samples per symbol.
|
||||
*/
|
||||
public float getSamplesPerSymbol()
|
||||
{
|
||||
return mSamplesPerSymbol;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.psk.demod;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Differential demodulator that uses scalar calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DifferentialDemodulatorFloatScalar extends DifferentialDemodulatorFloat
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatScalar.class);
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DifferentialDemodulatorFloatScalar(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
|
||||
|
||||
float[] decodedPhases = new float[i.length];
|
||||
float iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x++)
|
||||
{
|
||||
iPrevious = mIBuffer[x];
|
||||
qPreviousConjugate = -mQBuffer[x]; //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
iCurrent = mInterpolator.filter(mIBuffer, offset, mu);
|
||||
qCurrent = mInterpolator.filter(mQBuffer, offset, mu);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = (iPrevious * iCurrent) - (qPreviousConjugate * qCurrent);
|
||||
differentialQ = (iPrevious * qCurrent) + (iCurrent * qPreviousConjugate);
|
||||
|
||||
decodedPhases[x] = (float)Math.atan2(differentialQ, differentialI);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.psk.demod;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Differential demodulator that uses Vector SIMD 128 calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DifferentialDemodulatorFloatVector128 extends DifferentialDemodulatorFloat
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatVector128.class);
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DifferentialDemodulatorFloatVector128(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[i.length];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.psk.demod;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Differential demodulator that uses Vector SIMD calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DifferentialDemodulatorFloatVector256 extends DifferentialDemodulatorFloat
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DifferentialDemodulatorFloatVector256(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[sampleLength];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.psk.demod;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Differential demodulator that uses Vector SIMD 512 calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DifferentialDemodulatorFloatVector512 extends DifferentialDemodulatorFloat
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatVector512.class);
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DifferentialDemodulatorFloatVector512(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
return getScalarImplementation().demodulate(i,q);
|
||||
}
|
||||
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[sampleLength];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.psk.demod;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Differential demodulator that uses Vector SIMD 64 calculations for demodulating the sample stream.
|
||||
*/
|
||||
public class DifferentialDemodulatorFloatVector64 extends DifferentialDemodulatorFloat
|
||||
{
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatVector64.class);
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param sampleRate in Hertz
|
||||
* @param symbolRate symbols per second
|
||||
*/
|
||||
public DifferentialDemodulatorFloatVector64(double sampleRate, int symbolRate)
|
||||
{
|
||||
super(sampleRate, symbolRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] demodulate(float[] i, float[] q)
|
||||
{
|
||||
int sampleLength = i.length;
|
||||
int bufferOverlap = mBufferOverlap;
|
||||
float mu = mMu;
|
||||
|
||||
//Copy previous buffer residual samples to beginning of buffer.
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
|
||||
|
||||
//Resize I/Q buffers if necessary
|
||||
int requiredBufferLength = sampleLength + bufferOverlap;
|
||||
if(mIBuffer.length != requiredBufferLength)
|
||||
{
|
||||
if(i.length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
|
||||
}
|
||||
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
|
||||
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
|
||||
}
|
||||
|
||||
//Append new samples to the residual samples from the previous buffer.
|
||||
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
|
||||
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
|
||||
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
|
||||
|
||||
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
|
||||
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
|
||||
float[] decodedPhases = new float[i.length];
|
||||
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
|
||||
|
||||
//Differential demodulation.
|
||||
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
|
||||
{
|
||||
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
|
||||
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
|
||||
|
||||
int offset = mInterpolationOffset + x;
|
||||
int index;
|
||||
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
|
||||
{
|
||||
index = offset + y;
|
||||
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
|
||||
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
|
||||
}
|
||||
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
|
||||
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
|
||||
|
||||
//Multiply current complex sample by the complex conjugate of previous complex sample.
|
||||
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
|
||||
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
|
||||
|
||||
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
|
||||
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
|
||||
}
|
||||
|
||||
return decodedPhases;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * 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.dsp.psk.pll;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Monitors decode sync events to adaptively control PLL loop gain.
|
||||
*/
|
||||
public class AdaptivePLLBandwidthMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLBandwidthMonitor.class);
|
||||
|
||||
private static final int MAX_SYNC_COUNT = PLLBandwidth.BW_200.getRangeEnd();
|
||||
private CostasLoop mCostasLoop;
|
||||
private FeedbackDecoder mFeedbackDecoder;
|
||||
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
|
||||
private int mSyncCount;
|
||||
|
||||
/**
|
||||
* Constructs an adaptive monitor to monitor the sync state of a decoder and adjust the gain
|
||||
* level of the costas loop accordingly.
|
||||
*
|
||||
* @param costasLoop to receive adaptive gain updates.
|
||||
* @param feedbackDecoder to rebroadcast frequency error source events
|
||||
*/
|
||||
public AdaptivePLLBandwidthMonitor(CostasLoop costasLoop, FeedbackDecoder feedbackDecoder)
|
||||
{
|
||||
mCostasLoop = costasLoop;
|
||||
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
|
||||
mCostasLoop.setFrequencyErrorProcessor(this);
|
||||
mFeedbackDecoder = feedbackDecoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync detection event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncDetected(int bitErrors)
|
||||
{
|
||||
mSyncCount++;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync loss event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
mSyncCount -= 2;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the gain state for a sync detect or sync loss
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
if(mSyncCount < 0)
|
||||
{
|
||||
mSyncCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(mSyncCount > MAX_SYNC_COUNT)
|
||||
{
|
||||
mSyncCount = MAX_SYNC_COUNT;
|
||||
return;
|
||||
}
|
||||
|
||||
PLLBandwidth gain = PLLBandwidth.fromSyncCount(mSyncCount);
|
||||
|
||||
//Only update the costas loop if the gain value changes
|
||||
if(gain != mPLLBandwidth)
|
||||
{
|
||||
mPLLBandwidth = gain;
|
||||
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the monitor and commands the PLL to use the highest gain level.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mSyncCount = 0;
|
||||
mPLLBandwidth = PLLBandwidth.BW_400;
|
||||
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFrequencyError(long frequencyError)
|
||||
{
|
||||
//Only rebroadcast the costas loop frequency error if the gain level has progressed above
|
||||
//a bandwidth of 300, meaning that we have received at least 4 sync events
|
||||
if(mPLLBandwidth != PLLBandwidth.BW_400 && mPLLBandwidth != PLLBandwidth.BW_300)
|
||||
{
|
||||
mFeedbackDecoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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,7 @@ import org.slf4j.LoggerFactory;
|
||||
/**
|
||||
* Monitors decode sync events to adaptively control frequency correction broadcasts.
|
||||
*/
|
||||
@Deprecated //Remove after P25P2 decoder gets updated
|
||||
public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FrequencyCorrectionSyncMonitor.class);
|
||||
@@ -98,7 +99,7 @@ public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFre
|
||||
@Override
|
||||
public void processFrequencyError(long frequencyError)
|
||||
{
|
||||
mFeedbackDecoder.broadcast(SourceEvent.pllFrequencyMeasurement(frequencyError));
|
||||
mFeedbackDecoder.broadcast(SourceEvent.carrierOffsetMeasurement(-frequencyError));
|
||||
|
||||
//Only rebroadcast as a frequency error measurement if the sync count is more than 2
|
||||
if(mSyncCount > 2)
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * 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.dsp.psk.pll;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
|
||||
/**
|
||||
* Utility for calculating phase inverted sync pattern derivatives for QPSK constellations
|
||||
*/
|
||||
public class QPSKPhaseInversionCalculator
|
||||
{
|
||||
public static void main(String[] args)
|
||||
{
|
||||
|
||||
//DMR sync patterns
|
||||
// String[] syncs = {"755FD7DF75F7", "DFF57D75DF5D", "7F7D5DD57DFD", "D5D7F77FD757", "77D55F7DFD77",
|
||||
// "5D577F7757FF", "F7FDD5DDFD55", "7DFFD5F55D5F", "D7557F5FF7F5", "DD7FF5D757DD"};
|
||||
|
||||
//P25 Phase 2 sync pattern
|
||||
String[] syncs = {"575D57F7FF"};
|
||||
|
||||
//P25 Phase 1 sync pattern
|
||||
// String[] syncs = {"5575F5FF77FF"};
|
||||
|
||||
|
||||
for(String sync: syncs)
|
||||
{
|
||||
StringBuilder sbNorm = new StringBuilder();
|
||||
StringBuilder sbP90 = new StringBuilder();
|
||||
StringBuilder sbM90 = new StringBuilder();
|
||||
StringBuilder sb180 = new StringBuilder();
|
||||
|
||||
for(char letter: sync.toCharArray())
|
||||
{
|
||||
int value;
|
||||
|
||||
if(Character.isAlphabetic(letter))
|
||||
{
|
||||
value = letter - 55;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = letter - 48;
|
||||
}
|
||||
|
||||
sbNorm.append(letter);
|
||||
sbP90.append(rotate(value, Dibit.Rotation.PLUS90));
|
||||
sbM90.append(rotate(value, Dibit.Rotation.MINUS90));
|
||||
sb180.append(rotate(value, Dibit.Rotation.INVERT));
|
||||
}
|
||||
|
||||
System.out.println("NORMAL: " + sync);
|
||||
System.out.println(" +90: " + sbP90);
|
||||
System.out.println(" -90: " + sbM90);
|
||||
System.out.println(" 180: " + sb180);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the hexadecimal value to an integer, decomposes it into high and low order dibits, rotates the dibits
|
||||
* in the specified direction, and recombines the rotated dibits into a 4-bit value, returned as a hex character.
|
||||
* @param value to rotate
|
||||
* @param direction direction
|
||||
* @return rotated hex character
|
||||
*/
|
||||
public static String rotate(int value, Dibit.Rotation direction)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dibit dibitHigh = getHighDibit(value);
|
||||
Dibit dibitLow = getLowDibit(value);
|
||||
return getHex(dibitHigh.rotate(direction), dibitLow.rotate(direction));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//ignore
|
||||
}
|
||||
|
||||
return "-";
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the bit values from the two dibits and converts the integer value to a hex character
|
||||
* @param d1 containing the high order bits
|
||||
* @param d2 containing the low order bits
|
||||
* @return string hex character representing the value of these dibits combined
|
||||
*/
|
||||
public static String getHex(Dibit d1, Dibit d2)
|
||||
{
|
||||
int value = d1.getHighValue() + d2.getLowValue();
|
||||
return Integer.toHexString(value).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dibit that represents the bits in the 0x8 and 0x4 bit positions of the value
|
||||
* @param value
|
||||
* @return a dibit
|
||||
*/
|
||||
public static Dibit getHighDibit(int value)
|
||||
{
|
||||
return Dibit.fromValue((value >> 2) & 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dibit that represents the bits in the 0x2 and 0x1 bit positions of the value
|
||||
* @param value
|
||||
* @return a dibit
|
||||
*/
|
||||
public static Dibit getLowDibit(int value)
|
||||
{
|
||||
return Dibit.fromValue(value & 3);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.sample.Listener;
|
||||
|
||||
/**
|
||||
* Interface for a decoder to expose access to noise squelch controls and status registration endpoint(s).
|
||||
*/
|
||||
public interface INoiseSquelchController
|
||||
{
|
||||
/**
|
||||
* Registers the listener to receive noise squelch states every 10 milliseconds for UI display.
|
||||
* @param listener to receive states or pass null to de-register a listener.
|
||||
*/
|
||||
void setNoiseSquelchStateListener(Listener<NoiseSquelchState> listener);
|
||||
|
||||
/**
|
||||
* Sets the open and close noise thresholds in range 0.0 to 1.0, recommend: 0.1 open and 0.2 close
|
||||
* @param open for the open noise variance calculation.
|
||||
* @param close for the close noise variance calculation.
|
||||
*/
|
||||
void setNoiseThreshold(float open, float close);
|
||||
|
||||
/**
|
||||
* Sets the hysteresis threshold in units of 10 milliseconds
|
||||
* @param open in range 1-10, recommend: 4
|
||||
* @param close in range 1-10, recommend: 6
|
||||
*/
|
||||
void setHysteresisThreshold(int open, int close);
|
||||
|
||||
/**
|
||||
* Manual override of the squelch control to set an always un-squelch state (true) or automatic control (false).
|
||||
* @param override (true) or (false) to turn off squelch override.
|
||||
*/
|
||||
void setSquelchOverride(boolean override);
|
||||
}
|
||||
529
src/main/java/io/github/dsheirer/dsp/squelch/NoiseSquelch.java
Normal file
529
src/main/java/io/github/dsheirer/dsp/squelch/NoiseSquelch.java
Normal file
@@ -0,0 +1,529 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
|
||||
import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Squelch processor for unfiltered, demodulated audio that operators by high-pass filtering the audio to remove below
|
||||
* 3,000 Hertz and processes 10-millisecond audio buffers to calculate the noise variance across the samples. This
|
||||
* variance is compared to a noise threshold and uses time hysteresis in the form of consecutive buffers to toggle the
|
||||
* squelch open or closed. Uses a delay buffer equal in length to the hysteresis plus two which enables locating the
|
||||
* precise transition point of squelch/un-squelch and passing the audio of the detected signal while blocking the audio
|
||||
* when there is no signal detected. Register an audio listener to receive squelch controlled audio and a squelch state
|
||||
* listener to receive squelch toggle indications.
|
||||
*/
|
||||
public class NoiseSquelch implements INoiseSquelchController
|
||||
{
|
||||
public static final int DEFAULT_HYSTERESIS_OPEN_THRESHOLD = 4;
|
||||
public static final int DEFAULT_HYSTERESIS_CLOSE_THRESHOLD = 6;
|
||||
public static final int MINIMUM_HYSTERESIS_THRESHOLD = 1;
|
||||
public static final int MAXIMUM_HYSTERESIS_THRESHOLD = 10;
|
||||
public static final int VARIANCE_CALCULATION_WINDOW_MILLISECONDS = 10;
|
||||
public static final float DEFAULT_NOISE_OPEN_THRESHOLD = 0.1f;
|
||||
public static final float DEFAULT_NOISE_CLOSE_THRESHOLD = 0.19f;
|
||||
public static final float MINIMUM_NOISE_THRESHOLD = 0.1f;
|
||||
public static final float MAXIMUM_NOISE_THRESHOLD = 0.5f;
|
||||
private float[] mFilteredBuffer = new float[0];
|
||||
private float[] mAudioBuffer = new float[0];
|
||||
private float mMeanAccumulator;
|
||||
private float mNoiseOpenThreshold = DEFAULT_NOISE_OPEN_THRESHOLD;
|
||||
private float mNoiseCloseThreshold = DEFAULT_NOISE_CLOSE_THRESHOLD;
|
||||
private boolean mSquelch = true;
|
||||
private boolean mSquelchOverride = false;
|
||||
private int mMeanAccumulatorPointer;
|
||||
private int mVarianceWindowSize;
|
||||
private int mHysteresisOpenThreshold = DEFAULT_HYSTERESIS_OPEN_THRESHOLD;
|
||||
private int mHysteresisCloseThreshold = DEFAULT_HYSTERESIS_CLOSE_THRESHOLD;
|
||||
private int mHysteresisCount = 0;
|
||||
private int mSquelchStateBroadcastCounter = 0;
|
||||
private int mSquelchOpenIndex = 0;
|
||||
private int mAudioBufferFilterDelay;
|
||||
private IRealFilter mHighPassFilter;
|
||||
private Listener<float[]> mAudioListener;
|
||||
private Listener<SquelchState> mSquelchStateListener;
|
||||
private Listener<NoiseSquelchState> mNoiseSquelchStateListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param noiseOpen in range 0.0 to 1.0 - recommend 0.1 to 0.2 (units of demodulated, high-pass filtered audio
|
||||
* sample variance where the high-pass filtered audio samples range -1.0 to 1.0 and maximum variance is half of that
|
||||
* range: 1.0)
|
||||
* @param hysteresisOpen in range 1 to 10 controls the squelch toggle hysteresis - recommend 4 (units of 10 ms)
|
||||
*/
|
||||
public NoiseSquelch(float noiseOpen, float noiseClose, int hysteresisOpen, int hysteresisClose)
|
||||
{
|
||||
setNoiseThreshold(noiseOpen, noiseClose);
|
||||
setHysteresisThreshold(hysteresisOpen, hysteresisClose);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the current state is squelched.
|
||||
* @return true if squelched and false if not squelched.
|
||||
*/
|
||||
public boolean isSquelched()
|
||||
{
|
||||
return mSquelch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the manual squelch override.
|
||||
* @param override (true) or (false) to turn off manual squelch override.
|
||||
*/
|
||||
@Override
|
||||
public void setSquelchOverride(boolean override)
|
||||
{
|
||||
mSquelchOverride = override;
|
||||
|
||||
if(mSquelchOverride)
|
||||
{
|
||||
broadcast(SquelchState.UNSQUELCH);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the noise thresholds
|
||||
* @param open in range 0.0-1.0 and less than or equal to close, recommend 0.1
|
||||
* @param close in range 0.0-1.0 and greater than or equal to open, recommend 0.2
|
||||
*/
|
||||
public void setNoiseThreshold(float open, float close)
|
||||
{
|
||||
if(open < MINIMUM_NOISE_THRESHOLD || close > MAXIMUM_NOISE_THRESHOLD || open > close)
|
||||
{
|
||||
throw new IllegalArgumentException("Noise thresholds open/close [" + open + "/" + close + "] - open must be less than or equal to close and both in range " + MINIMUM_NOISE_THRESHOLD + "-" + MAXIMUM_NOISE_THRESHOLD);
|
||||
}
|
||||
|
||||
mNoiseOpenThreshold = open;
|
||||
mNoiseCloseThreshold = close;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the hysteresis thresholds
|
||||
* @param open in range 1 to 10 and less than or equal to close, recommend 4.
|
||||
* @param close in range 1 to 10 and greater than or equal to open, recommend 6.
|
||||
*/
|
||||
public void setHysteresisThreshold(int open, int close)
|
||||
{
|
||||
if(open < MINIMUM_HYSTERESIS_THRESHOLD || close > MAXIMUM_HYSTERESIS_THRESHOLD || open > close)
|
||||
{
|
||||
throw new IllegalArgumentException("Hysteresis threshold open [" + open + "] must be less than or equal to close [" +
|
||||
close + "] and both in range " + MINIMUM_HYSTERESIS_THRESHOLD + "-" + MAXIMUM_HYSTERESIS_THRESHOLD);
|
||||
}
|
||||
|
||||
mHysteresisOpenThreshold = open;
|
||||
mHysteresisCloseThreshold = close;
|
||||
mHysteresisCount = Math.min(mHysteresisCloseThreshold, mHysteresisCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive noise squelch state updates every 100 milliseconds.
|
||||
* @param listener to register, or de-register by passing in a null.
|
||||
*/
|
||||
public void setNoiseSquelchStateListener(Listener<NoiseSquelchState> listener)
|
||||
{
|
||||
mNoiseSquelchStateListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive squelch state indications. Squelch state events will be sent to the listener
|
||||
* in concert with processed audio buffers. An un-squelch event is followed by one or more audio buffers and then a
|
||||
* squelch event.
|
||||
* @param listener to register or de-register by passing in a null.
|
||||
*/
|
||||
public void setSquelchStateListener(Listener<SquelchState> listener)
|
||||
{
|
||||
mSquelchStateListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive processed audio buffers in between un-squelch/squelch events.
|
||||
* @param listener to register or de-register by passing in a null.
|
||||
*/
|
||||
public void setAudioListener(Listener<float[]> listener)
|
||||
{
|
||||
mAudioListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the current noise squelch state to an optionally registered listener. This can be use used in the
|
||||
* user interface to display the state of the channel and noise squelch activity.
|
||||
* @param noise variance from the current 10-millisecond audio buffer.
|
||||
*/
|
||||
private void broadcastNoiseSquelchState(float noise)
|
||||
{
|
||||
if(mNoiseSquelchStateListener != null)
|
||||
{
|
||||
mNoiseSquelchStateListener.receive(new NoiseSquelchState(mSquelch, mSquelchOverride, noise,
|
||||
mNoiseOpenThreshold, mNoiseCloseThreshold, mHysteresisCount, mHysteresisOpenThreshold,
|
||||
mHysteresisCloseThreshold));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts from the unfiltered audio buffer to an optionally registered audio listener. Note: the start and end
|
||||
* indices are pointers to the filtered audio buffer, and we dispatch from the unfiltered audio buffer which is offset
|
||||
* by half the length of the high-pass audio filter coefficients. This method adjusts the index arguments by this
|
||||
* offset value to align the unfiltered audio with the high-pass filtered audio detection boundaries.
|
||||
*
|
||||
* @param start index of the filtered audio buffer
|
||||
* @param end index of the filtered audio buffer
|
||||
*/
|
||||
private void broadcast(int start, int end)
|
||||
{
|
||||
if(mAudioListener != null)
|
||||
{
|
||||
mAudioListener.receive(Arrays.copyOfRange(mAudioBuffer, start, end));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the squelch state change to an optionally registered squelch state listener.
|
||||
* @param squelchState to dispatch.
|
||||
*/
|
||||
private void broadcast(SquelchState squelchState)
|
||||
{
|
||||
//Override squelch state when we're in manual override
|
||||
if(squelchState == SquelchState.SQUELCH && mSquelchOverride)
|
||||
{
|
||||
squelchState = SquelchState.UNSQUELCH;
|
||||
}
|
||||
|
||||
if(mSquelchStateListener != null)
|
||||
{
|
||||
mSquelchStateListener.receive(squelchState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sample rate for the incoming demodulated audio sample stream.
|
||||
* @param sampleRate of the sample stream.
|
||||
*/
|
||||
public void setSampleRate(double sampleRate)
|
||||
{
|
||||
float[] coefficients = FilterFactory.getHighPass((int)sampleRate, 3000, 31, WindowType.BLACKMAN_HARRIS_7);
|
||||
mHighPassFilter = new RealFIRFilter(coefficients);
|
||||
mVarianceWindowSize = (int)(sampleRate * (VARIANCE_CALCULATION_WINDOW_MILLISECONDS / 1000.0));
|
||||
|
||||
//Offset for the filter delay between the audio buffer and the filtered audio noise detection buffer.
|
||||
mAudioBufferFilterDelay = (int)Math.ceil(coefficients.length / 2.0);
|
||||
}
|
||||
|
||||
public void process(float[] samples)
|
||||
{
|
||||
float[] filtered = mHighPassFilter.filter(samples);
|
||||
|
||||
int requiredLength = filtered.length + (mVarianceWindowSize * (mHysteresisCloseThreshold + 2));
|
||||
|
||||
//Resize the delay buffers if the incoming sample buffer size changes.
|
||||
if(mFilteredBuffer.length != requiredLength)
|
||||
{
|
||||
mFilteredBuffer = Arrays.copyOf(mFilteredBuffer, requiredLength);
|
||||
|
||||
//The audio buffer is longer to account for filter delay of the high pass filter and to align the indices
|
||||
//across the filtered and audio buffers.
|
||||
mAudioBuffer = Arrays.copyOf(mAudioBuffer, requiredLength + mAudioBufferFilterDelay);
|
||||
}
|
||||
|
||||
//Shuffle the samples to the left in the delay buffers and copy in the new samples.
|
||||
int length = mFilteredBuffer.length - filtered.length;
|
||||
System.arraycopy(mFilteredBuffer, filtered.length, mFilteredBuffer, 0, length);
|
||||
System.arraycopy(filtered, 0, mFilteredBuffer, length, filtered.length);
|
||||
|
||||
length = mAudioBuffer.length - samples.length;
|
||||
System.arraycopy(mAudioBuffer, samples.length, mAudioBuffer, 0, length);
|
||||
System.arraycopy(samples, 0, mAudioBuffer, length, samples.length);
|
||||
|
||||
int squelchCloseIndex;
|
||||
//Adjust the squelch open index in sync with the left-shuffle of the audio samples.
|
||||
|
||||
mSquelchOpenIndex = Math.max(mSquelchOpenIndex - samples.length, 0);
|
||||
|
||||
//Process the newly added (filtered) samples in the delay buffer.
|
||||
for(int x = mFilteredBuffer.length - filtered.length; x < mFilteredBuffer.length; x++)
|
||||
{
|
||||
mMeanAccumulator += mFilteredBuffer[x];
|
||||
mMeanAccumulatorPointer++;
|
||||
|
||||
if(mMeanAccumulatorPointer == mVarianceWindowSize)
|
||||
{
|
||||
float mean = mMeanAccumulator / mVarianceWindowSize;
|
||||
|
||||
//Reset averaging variables
|
||||
mMeanAccumulator = 0.0f;
|
||||
mMeanAccumulatorPointer = 0;
|
||||
|
||||
//Calculate the variance
|
||||
float varianceAccumulator1 = 0.0f, deviation, varianceAccumulator2 = 0.0f;
|
||||
for(int i = x - mVarianceWindowSize + 1; i <= x; i++)
|
||||
{
|
||||
deviation = mFilteredBuffer[i] - mean;
|
||||
varianceAccumulator1 += deviation * deviation;
|
||||
varianceAccumulator2 += deviation;
|
||||
}
|
||||
|
||||
//Formula from Apache Commons Math - Variance class.
|
||||
float noiseVariance = (varianceAccumulator1 - (varianceAccumulator2 * varianceAccumulator2 / mVarianceWindowSize)) / mVarianceWindowSize;
|
||||
boolean below = mSquelch ? (noiseVariance < mNoiseOpenThreshold) : (noiseVariance < mNoiseCloseThreshold);
|
||||
mHysteresisCount += (below ? 1 : -1);
|
||||
|
||||
if(mSquelch && mHysteresisCount >= mHysteresisOpenThreshold)
|
||||
{
|
||||
mSquelch = false;
|
||||
mHysteresisCount = mHysteresisOpenThreshold;
|
||||
mSquelchOpenIndex = findTransition(x);
|
||||
|
||||
if(!mSquelchOverride)
|
||||
{
|
||||
broadcast(SquelchState.UNSQUELCH);
|
||||
}
|
||||
}
|
||||
else if(!mSquelch && mHysteresisCount <= 0)
|
||||
{
|
||||
mSquelch = true;
|
||||
mHysteresisCount = 0;
|
||||
squelchCloseIndex = findTransition(x);
|
||||
|
||||
//Broadcast the partial audio segment ending at the transition point.
|
||||
if(mSquelchOpenIndex < squelchCloseIndex)
|
||||
{
|
||||
//Only broadcast the truncated audio samples when we're not in squelch override
|
||||
if(!mSquelchOverride)
|
||||
{
|
||||
broadcast(mSquelchOpenIndex, squelchCloseIndex);
|
||||
}
|
||||
|
||||
broadcast(SquelchState.SQUELCH);
|
||||
}
|
||||
|
||||
mSquelchOpenIndex = 0;
|
||||
}
|
||||
else if(mSquelch && !mSquelchOverride)
|
||||
{
|
||||
int start = x - (mVarianceWindowSize * mHysteresisOpenThreshold) + 1;
|
||||
int end = start + mVarianceWindowSize;
|
||||
//Make sure start doesn't go negative
|
||||
start = Math.max(0, start);
|
||||
Arrays.fill(mAudioBuffer, start, end, 0.0f);
|
||||
}
|
||||
|
||||
mHysteresisCount = Math.min(mHysteresisCount, mHysteresisCloseThreshold);
|
||||
mHysteresisCount = Math.max(mHysteresisCount, 0);
|
||||
|
||||
//Broadcast squelch state to an optionally registered listener (the channel tab in user interface)
|
||||
mSquelchStateBroadcastCounter++;
|
||||
if(mSquelchStateBroadcastCounter >= 5)
|
||||
{
|
||||
broadcastNoiseSquelchState(noiseVariance);
|
||||
mSquelchStateBroadcastCounter -= 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mSquelchOverride)
|
||||
{
|
||||
broadcast(0, samples.length);
|
||||
}
|
||||
else if(!mSquelch && mSquelchOpenIndex < (samples.length))
|
||||
{
|
||||
mSquelchOpenIndex = Math.max(mSquelchOpenIndex, 0);
|
||||
//Dispatch audio after processing the audio buffer if we end in an un-squelch state.
|
||||
broadcast(mSquelchOpenIndex, samples.length);
|
||||
mSquelchOpenIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the squelch transition point across a hysteresis window delayed by 2x buffer periods from the current
|
||||
* processing index. This enables fine-grained squelch control without losing audio during the un-squelch period or
|
||||
* allowing noise to leak into the squelch period at the end of the transmission. Uses a reduced 20-sample variance
|
||||
* calculation window extended to the hysteresis time period to detect the transition point. Identifies the
|
||||
* transition point within +/- 20 audio samples.
|
||||
*
|
||||
* @param current buffer processing index.
|
||||
* @return buffer index of detected transition.
|
||||
*/
|
||||
private int findTransition(int current)
|
||||
{
|
||||
//Search window is 10-ms buffer size extended to hysteresis period, delayed by 2x buffer lengths.
|
||||
int window = mSquelch ? mHysteresisCloseThreshold : mHysteresisOpenThreshold;
|
||||
|
||||
//Thresholds here may seem reversed but keep in mind that the mSquelch state has already been flipped and thus
|
||||
// represents the state moving forward, and we need to detect the transition from the previous mSquelch state.
|
||||
float varianceThreshold = mSquelch ? mNoiseCloseThreshold : mNoiseOpenThreshold;
|
||||
int hysteresisThreshold = mSquelch ? mHysteresisCloseThreshold : mHysteresisOpenThreshold;
|
||||
|
||||
//Look back window start and end indices are sized according the previous mSquelch state
|
||||
int start = current - (mVarianceWindowSize * (window + 2));
|
||||
int end = start + (mVarianceWindowSize * window);
|
||||
|
||||
int hysteresisCount = 0;
|
||||
int windowSize = mVarianceWindowSize / 5; //Decrease transition variance window to 1/5th of the normal variance window
|
||||
int transitionPointer = 0;
|
||||
|
||||
for(int x = start; x < end; x += windowSize)
|
||||
{
|
||||
float mean = getMean(x, x + windowSize);
|
||||
float variance = getVariance(x, x + windowSize, mean);
|
||||
|
||||
//Detect transition from from un-squelch to squelch
|
||||
boolean exceedsThreshold = (mSquelch ? (variance > varianceThreshold) : (variance < varianceThreshold));
|
||||
|
||||
if(exceedsThreshold)
|
||||
{
|
||||
if(hysteresisCount == 0)
|
||||
{
|
||||
hysteresisCount++;
|
||||
transitionPointer = x;
|
||||
}
|
||||
else
|
||||
{
|
||||
hysteresisCount++;
|
||||
|
||||
if(hysteresisCount >= hysteresisThreshold)
|
||||
{
|
||||
return transitionPointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
hysteresisCount--;
|
||||
hysteresisCount = Math.max(0, hysteresisCount);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the mean/average of values between the start and end indices.
|
||||
* @param start index
|
||||
* @param end index
|
||||
* @return mean value.
|
||||
*/
|
||||
private float getMean(int start, int end)
|
||||
{
|
||||
if(start < 0)
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
if(end > mFilteredBuffer.length)
|
||||
{
|
||||
end = mFilteredBuffer.length;
|
||||
}
|
||||
|
||||
float accumulator = 0.0f;
|
||||
|
||||
for(int x = start; x < end; x++)
|
||||
{
|
||||
accumulator += mFilteredBuffer[x];
|
||||
}
|
||||
|
||||
return accumulator / (end - start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the variance of the values between start and end indices using the supplied mean value.
|
||||
* @param start index
|
||||
* @param end index
|
||||
* @param mean of values between start and end, calculated previously
|
||||
* @return variance.
|
||||
*/
|
||||
private float getVariance(int start, int end, float mean)
|
||||
{
|
||||
if(start < 0)
|
||||
{
|
||||
start = 0;
|
||||
}
|
||||
|
||||
if(end > mFilteredBuffer.length)
|
||||
{
|
||||
end = mFilteredBuffer.length;
|
||||
}
|
||||
|
||||
float varianceAccumulator1 = 0.0f, deviation, varianceAccumulator2 = 0.0f;
|
||||
|
||||
for(int i = start; i < end; i++)
|
||||
{
|
||||
deviation = mFilteredBuffer[i] - mean;
|
||||
varianceAccumulator1 += deviation * deviation;
|
||||
varianceAccumulator2 += deviation;
|
||||
}
|
||||
|
||||
int sampleSize = end - start;
|
||||
|
||||
//Formula from Apache Commons Math - Variance class.
|
||||
return (varianceAccumulator1 - (varianceAccumulator2 * varianceAccumulator2 / sampleSize)) / sampleSize;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// Path directory = Paths.get("/media/denny/T9/NBFM Squelch Research"); //Linux
|
||||
// Path file = directory.resolve("20250629_072704_453850000_Onondaga-County_Fire-EMS_OC-Fire-Disp_79_baseband.wav");
|
||||
// Path file = directory.resolve("DMR_1_CAPPLUS.wav");
|
||||
// Path file = directory.resolve("20250702_034109_145510000_R828D_V4_POLY_No_Antenna_Normal_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034301_145510000_R828D_V4_POLY_No_Antenna_Max_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034403_145510000_R828D_V4_POLY_No_Antenna_Min_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034541_145510000_R828D_V4_HET_No_Antenna_Min_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034635_145510000_R828D_V4_HET_No_Antenna_Max_Gain.wav");
|
||||
// Path file = directory.resolve("20250702_034742_145510000_R828D_V4_HET_No_Antenna_Normal_Gain.wav");
|
||||
// Path file = directory.resolve("20250703_043912_162500000_NOAA_25_khz_no_center_signal.wav");
|
||||
// Path file = directory.resolve("20250703_043958_162550000_NOAA_25_khz_yes_center_signal.wav");
|
||||
//
|
||||
// DecodeConfigNBFM config = new DecodeConfigNBFM();
|
||||
// config.setBandwidth(DecodeConfigAnalog.Bandwidth.BW_12_5);
|
||||
// NBFMDecoder decoder = new NBFMDecoder(config);
|
||||
//
|
||||
// try(ComplexWaveSource source = new ComplexWaveSource(file.toFile(), false))
|
||||
// {
|
||||
// source.setListener(iNativeBuffer -> {
|
||||
// Iterator<ComplexSamples> it = iNativeBuffer.iterator();
|
||||
//
|
||||
// while(it.hasNext())
|
||||
// {
|
||||
// ComplexSamples samples = it.next();
|
||||
// decoder.receive(samples);
|
||||
// }
|
||||
// });
|
||||
// source.start();
|
||||
//
|
||||
// decoder.getSourceEventListener().receive(SourceEvent.sampleRateChange(source.getSampleRate()));
|
||||
//
|
||||
// while(true)
|
||||
// {
|
||||
// source.next(2048, true);
|
||||
// }
|
||||
// }
|
||||
// catch(IOException ioe)
|
||||
// {
|
||||
//
|
||||
// ioe.printStackTrace();
|
||||
// }
|
||||
//
|
||||
// System.out.println("Finished");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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;
|
||||
|
||||
/**
|
||||
* Noise Squelch operating status record. Captures a snapshot in time of the squelch status.
|
||||
* @param squelch state, on (true) or off (false)
|
||||
* @param squelchOverride on (true) or off (false)
|
||||
* @param noise - current noise variance
|
||||
* @param noiseOpenThreshold for determining un-squelch state
|
||||
* @param noiseCloseThreshold for determining squelch state
|
||||
* @param hysteresis in units of 10-milliseconds
|
||||
* @param hysteresisOpenThreshold for toggling the squelch state.
|
||||
* @param hysteresisCloseThreshold for toggling the squelch state.
|
||||
*/
|
||||
public record NoiseSquelchState(boolean squelch, boolean squelchOverride, float noise, float noiseOpenThreshold,
|
||||
float noiseCloseThreshold, int hysteresis, int hysteresisOpenThreshold,
|
||||
int hysteresisCloseThreshold){}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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,14 +34,14 @@ public class PowerMonitor
|
||||
private int mPowerLevelBroadcastCount = 0;
|
||||
private int mPowerLevelBroadcastThreshold;
|
||||
private Listener<SourceEvent> mSourceEventListener;
|
||||
private SinglePoleIirFilter mPowerFilter = new SinglePoleIirFilter(0.1f);
|
||||
private final SinglePoleIirFilter mPowerFilter = new SinglePoleIirFilter(0.1f);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public PowerMonitor()
|
||||
{
|
||||
mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz
|
||||
mPowerLevelBroadcastThreshold = 12500; //Based on a default sample rate of 25 kHz
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,36 +54,25 @@ public class PowerMonitor
|
||||
mPowerLevelBroadcastThreshold = sampleRate / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
mPowerLevelBroadcastCount++;
|
||||
|
||||
if(mPowerLevelBroadcastCount > mPowerLevelBroadcastThreshold)
|
||||
{
|
||||
mPowerFilter.filter(inphase * inphase + quadrature * quadrature);
|
||||
}
|
||||
|
||||
if(mPowerLevelBroadcastCount > (mPowerLevelBroadcastThreshold + 10))
|
||||
{
|
||||
mPowerLevelBroadcastCount = 0;
|
||||
broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPowerFilter.getValue())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes I&Q complex baseband sample buffers.
|
||||
*/
|
||||
public void process(float[] i, float[] q)
|
||||
{
|
||||
for(int x = 0; x < i.length; x++)
|
||||
mPowerLevelBroadcastCount += i.length;
|
||||
|
||||
if(mPowerLevelBroadcastCount > mPowerLevelBroadcastThreshold && i.length >= 10)
|
||||
{
|
||||
process(i[x], q[x]);
|
||||
double iSquared, qSquared;
|
||||
for(int x = 0; x < 10; x++)
|
||||
{
|
||||
iSquared = Math.pow(i[x], 2.0f);
|
||||
qSquared = Math.pow(q[x], 2.0f);
|
||||
mPowerFilter.filter((float)(iSquared + qSquared));
|
||||
}
|
||||
|
||||
mPowerLevelBroadcastCount -= mPowerLevelBroadcastThreshold;
|
||||
broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPowerFilter.getValue())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +87,7 @@ public class PowerMonitor
|
||||
/**
|
||||
* Broadcasts the source event to an optional register listener
|
||||
*/
|
||||
private void broadcast(SourceEvent event)
|
||||
public void broadcast(SourceEvent event)
|
||||
{
|
||||
if(mSourceEventListener != null)
|
||||
{
|
||||
|
||||
@@ -1,47 +1,55 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.symbol;
|
||||
|
||||
public enum Dibit
|
||||
{
|
||||
D01_PLUS_3(false, true, 1, 4, 1),
|
||||
D00_PLUS_1(false, false, 0, 0, 0),
|
||||
D10_MINUS_1(true, false, 2, 8, 2),
|
||||
D11_MINUS_3(true, true, 3, 12, 3);
|
||||
D01_PLUS_3(false, true, 1, 4, 1, (float)((Math.PI / 4) * 3)),
|
||||
D00_PLUS_1(false, false, 0, 0, 0, (float)(Math.PI / 4)),
|
||||
D10_MINUS_1(true, false, 2, 8, 2, (float)(Math.PI / -4)),
|
||||
D11_MINUS_3(true, true, 3, 12, 3, (float)((Math.PI / 4) * -3));
|
||||
|
||||
private boolean mBit1;
|
||||
private boolean mBit2;
|
||||
private int mLowValue;
|
||||
private int mHighValue;
|
||||
private int mValue;
|
||||
private float mPhase;
|
||||
|
||||
private Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value)
|
||||
/**
|
||||
* Dibit constructor.
|
||||
* @param bit1 boolean value
|
||||
* @param bit2 boolean value
|
||||
* @param lowValue integer value of the low order bit
|
||||
* @param highValue integer value of the high order bit
|
||||
* @param value integer value combined of both bits.
|
||||
* @param phase optimal phase value for this symbol when transmitted.
|
||||
*/
|
||||
Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value, float phase)
|
||||
{
|
||||
mBit1 = bit1;
|
||||
mBit2 = bit2;
|
||||
mLowValue = lowValue;
|
||||
mHighValue = highValue;
|
||||
mValue = value;
|
||||
mPhase = phase;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +60,15 @@ public enum Dibit
|
||||
return mValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ideal phase angle when this symbol is transmitted.
|
||||
* @return ideal phase angle in radians.
|
||||
*/
|
||||
public float getIdealPhase()
|
||||
{
|
||||
return mPhase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowable transition state 1 for trellis coded modulation. Indicates
|
||||
* the dibit that differs from this dibit in the LSB bit position.
|
||||
@@ -248,15 +265,24 @@ public enum Dibit
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
/**
|
||||
* Number of bit errors between this dibit and the expected dibit argument.
|
||||
* @param dibit to compare
|
||||
* @return 0, 1 or 2 bit errors
|
||||
*/
|
||||
public int getBitErrorFrom(Dibit dibit)
|
||||
{
|
||||
byte a = (byte)0x1B;
|
||||
int xor = getValue() ^ dibit.getValue();
|
||||
|
||||
Dibit d1 = Dibit.parse(a, 0);
|
||||
Dibit d2 = Dibit.parse(a, 1);
|
||||
Dibit d3 = Dibit.parse(a, 2);
|
||||
Dibit d4 = Dibit.parse(a, 3);
|
||||
if(xor == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if(xor == 3)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
System.out.println("1: " + d1 + " 2:" + d2 + " 3:" + d3 + " 4:" + d4);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * 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/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.symbol;
|
||||
|
||||
import io.github.dsheirer.bits.BitSetFullException;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Circular buffer for storing and accessing dibits.
|
||||
*/
|
||||
public class DibitDelayBuffer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DibitDelayBuffer.class);
|
||||
|
||||
protected Dibit[] mBuffer;
|
||||
protected int mPointer;
|
||||
|
||||
/**
|
||||
* Constructs a dibit delay buffer of the specified length
|
||||
*/
|
||||
public DibitDelayBuffer(int length)
|
||||
{
|
||||
Validate.isTrue(length > 0, "Dibit buffer length must be greater than 0");
|
||||
mBuffer = new Dibit[length];
|
||||
reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the buffer and preloads the buffer with all zeros
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
for(int x = 0; x < mBuffer.length; x++)
|
||||
{
|
||||
mBuffer[x] = Dibit.D00_PLUS_1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of this circular buffer
|
||||
*/
|
||||
public int length()
|
||||
{
|
||||
return mBuffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dibit at the specified index
|
||||
* @param index to get
|
||||
* @return dibit at the specified index
|
||||
*/
|
||||
public Dibit get(int index)
|
||||
{
|
||||
if(index < 0 || index >= mBuffer.length)
|
||||
{
|
||||
throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]");
|
||||
}
|
||||
|
||||
return mBuffer[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dibit at the specified index
|
||||
* @param index of the dibit
|
||||
* @param dibit to load at the specified index
|
||||
*/
|
||||
public void set(int index, Dibit dibit)
|
||||
{
|
||||
if(index < 0 || index >= mBuffer.length)
|
||||
{
|
||||
throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]");
|
||||
}
|
||||
|
||||
mBuffer[index] = dibit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ordered view of the internal circular buffer contents.
|
||||
*/
|
||||
public Dibit[] getBuffer()
|
||||
{
|
||||
Dibit[] transferBuffer = new Dibit[mBuffer.length];
|
||||
|
||||
int transferBufferPointer = 0;
|
||||
int bufferPointer = mPointer;
|
||||
|
||||
while(transferBufferPointer < transferBuffer.length)
|
||||
{
|
||||
transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++];
|
||||
|
||||
if(bufferPointer >= mBuffer.length)
|
||||
{
|
||||
bufferPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return transferBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a view of the buffer from the starting index to the specified length. Note: if the requested
|
||||
* length exceeds the length of this buffer, the contents will be unwrapped from the circular buffer and repeated.
|
||||
* @param start of the view where 0 is the first element.
|
||||
* @param length of the dibit view
|
||||
* @return an array of dibits
|
||||
*/
|
||||
public Dibit[] getBuffer(int start, int length)
|
||||
{
|
||||
Dibit[] transferBuffer = new Dibit[length];
|
||||
|
||||
int transferBufferPointer = 0;
|
||||
int bufferPointer = (mPointer + start) % mBuffer.length;
|
||||
|
||||
while(transferBufferPointer < transferBuffer.length)
|
||||
{
|
||||
transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++];
|
||||
|
||||
if(bufferPointer >= mBuffer.length)
|
||||
{
|
||||
bufferPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return transferBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a corrected binary message from the dibit buffer
|
||||
*
|
||||
* @param start dibit index where 0 is the oldest dibit and the newest dibit is buffer length - 1
|
||||
* @param dibitLength number of dibits to include in the message, making the message length = dibitLength * 2
|
||||
* @return corrected binary message
|
||||
*/
|
||||
public CorrectedBinaryMessage getMessage(int start, int dibitLength)
|
||||
{
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(dibitLength * 2);
|
||||
|
||||
int dibitCount = 0;
|
||||
int bufferPointer = (mPointer + start) % mBuffer.length;
|
||||
|
||||
try
|
||||
{
|
||||
while(dibitCount < dibitLength)
|
||||
{
|
||||
Dibit dibit = mBuffer[bufferPointer++];
|
||||
message.add(dibit.getBit1());
|
||||
message.add(dibit.getBit2());
|
||||
dibitCount++;
|
||||
|
||||
if(bufferPointer >= mBuffer.length)
|
||||
{
|
||||
bufferPointer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(BitSetFullException e)
|
||||
{
|
||||
//This shouldn't happen
|
||||
mLog.error("Bit set full ??", e);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to view the buffer contents with the dibits decoded as an array of zeros and ones.
|
||||
*/
|
||||
public int[] getBufferAsArray()
|
||||
{
|
||||
Dibit[] dibits = getBuffer();
|
||||
|
||||
int[] bits = new int[dibits.length * 2];
|
||||
|
||||
for(int x = 0; x < dibits.length; x++)
|
||||
{
|
||||
if(dibits[x].getBit1())
|
||||
{
|
||||
bits[x * 2] = 1;
|
||||
}
|
||||
if(dibits[x].getBit2())
|
||||
{
|
||||
bits[x * 2 + 1] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return bits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the dibit into the internal circular buffer, overwriting the oldest dibit.
|
||||
*/
|
||||
public void put(Dibit dibit)
|
||||
{
|
||||
mBuffer[mPointer++] = dibit;
|
||||
|
||||
if(mPointer >= mBuffer.length)
|
||||
{
|
||||
mPointer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the dibit into the internal circular buffer, overwriting and returning the
|
||||
* oldest dibit.
|
||||
*/
|
||||
public Dibit getAndPut(Dibit dibit)
|
||||
{
|
||||
Dibit toReturn = mBuffer[mPointer];
|
||||
put(dibit);
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the contents of the buffer
|
||||
*/
|
||||
public void log()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int counter = 0;
|
||||
int pointer = mPointer;
|
||||
|
||||
while(counter < mBuffer.length)
|
||||
{
|
||||
sb.append(mBuffer[pointer].getBit1() ? "1" : "0");
|
||||
sb.append(mBuffer[pointer++].getBit2() ? "1" : "0");
|
||||
|
||||
if(pointer >= mBuffer.length)
|
||||
{
|
||||
pointer = 0;
|
||||
}
|
||||
|
||||
counter++;
|
||||
}
|
||||
mLog.debug("BUFFER: " + sb + " Length:" + mBuffer.length);
|
||||
}
|
||||
}
|
||||
128
src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java
Normal file
128
src/main/java/io/github/dsheirer/dsp/symbol/DibitDelayLine.java
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.symbol;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Implements a circular buffer delay line for dibits.
|
||||
*/
|
||||
public class DibitDelayLine
|
||||
{
|
||||
protected Dibit[] mDelayLine;
|
||||
public int mPointer;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param length of the delay line in dibits.
|
||||
*/
|
||||
public DibitDelayLine(int length)
|
||||
{
|
||||
mDelayLine = new Dibit[length];
|
||||
Arrays.fill(mDelayLine, Dibit.D00_PLUS_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the dibit into the delay line and returns the oldest dibit from the delay line.
|
||||
* @param dibit to insert
|
||||
* @return oldest dibit
|
||||
*/
|
||||
public Dibit insert(Dibit dibit)
|
||||
{
|
||||
Dibit ejected = mDelayLine[mPointer];
|
||||
mDelayLine[mPointer++] = dibit;
|
||||
mPointer %= mDelayLine.length;
|
||||
return ejected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the most recently added dibits in the delay line with the provided sequence. This can be used when
|
||||
* a sync pattern is detected to overwrite the calculated symbols with the known sync pattern.
|
||||
* @param dibits to overwrite
|
||||
*/
|
||||
public void update(Dibit[] dibits)
|
||||
{
|
||||
mPointer = mPointer - dibits.length;
|
||||
|
||||
if(mPointer < 0)
|
||||
{
|
||||
mPointer += mDelayLine.length;
|
||||
}
|
||||
|
||||
for(Dibit dibit: dibits)
|
||||
{
|
||||
mDelayLine[mPointer++] = dibit;
|
||||
mPointer %= mDelayLine.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the pointer to correct a dibit stuff/delete.
|
||||
* @param offset to adjust.
|
||||
*/
|
||||
public void adjustPointer(int offset)
|
||||
{
|
||||
mPointer += offset;
|
||||
|
||||
if(mPointer < 0)
|
||||
{
|
||||
mPointer += mDelayLine.length;
|
||||
}
|
||||
else if(mPointer >= mDelayLine.length)
|
||||
{
|
||||
mPointer -= mDelayLine.length;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the most recently stored Dibit.
|
||||
*/
|
||||
public Dibit getLast()
|
||||
{
|
||||
int pointer = mPointer - 1;
|
||||
|
||||
if(pointer < 0)
|
||||
{
|
||||
pointer += mDelayLine.length;
|
||||
}
|
||||
|
||||
return mDelayLine[pointer];
|
||||
}
|
||||
|
||||
public void log()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
int pointer = mPointer;
|
||||
for(int i = 0; i < mDelayLine.length; i++)
|
||||
{
|
||||
Dibit dibit = mDelayLine[pointer];
|
||||
sb.append(dibit.getBit1() ? "1" : "0");
|
||||
sb.append(dibit.getBit2() ? "1" : "0");
|
||||
sb.append(" ");
|
||||
|
||||
pointer++;
|
||||
pointer = pointer % mDelayLine.length;
|
||||
}
|
||||
|
||||
System.out.println(sb);
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class BCH_63_16_11 extends ReedSolomon_63_P25
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BCH_63_16_11.class);
|
||||
|
||||
/**
|
||||
* BCH( 63,16,11) decoder
|
||||
*/
|
||||
public BCH_63_16_11()
|
||||
{
|
||||
super(63, 41);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction on the first 63 bits of the
|
||||
* message argument. If the message is correctable, only the first 16 bits
|
||||
* (information bits) are corrected.
|
||||
*
|
||||
* @return - true = success, false = failure
|
||||
*/
|
||||
public BinaryMessage correctNID(BinaryMessage message)
|
||||
{
|
||||
CRC status = CRC.PASSED;
|
||||
|
||||
int[] original = message.toReverseIntegerArray(0, 62);
|
||||
int[] corrected = new int[63];
|
||||
|
||||
boolean irrecoverableErrors = decode(original, corrected);
|
||||
|
||||
if(irrecoverableErrors)
|
||||
{
|
||||
message.setCRC(CRC.FAILED_CRC);
|
||||
|
||||
return message;
|
||||
}
|
||||
else
|
||||
{
|
||||
for(int x = 0; x < 16; x++)
|
||||
{
|
||||
int index = 63 - x - 1;
|
||||
|
||||
if(corrected[index] != original[index])
|
||||
{
|
||||
status = CRC.CORRECTED;
|
||||
|
||||
if(corrected[index] == 1)
|
||||
{
|
||||
message.set(x);
|
||||
}
|
||||
else
|
||||
{
|
||||
message.clear(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.setCRC(status);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String orig = "0010011000000011010010100000000110000111110011101010001010110000";
|
||||
String error = "0001010100000011010010100000000110000111100011001010001010110000";
|
||||
|
||||
BinaryMessage errorMessage = BinaryMessage.load(error);
|
||||
|
||||
BCH_63_16_11 bch = new BCH_63_16_11();
|
||||
|
||||
mLog.debug("ORIG:" + orig);
|
||||
mLog.debug(" ERR:" + errorMessage.toString());
|
||||
mLog.debug(" ");
|
||||
|
||||
errorMessage = bch.correctNID(errorMessage);
|
||||
|
||||
mLog.debug(" CRC: " + errorMessage.getCRC().name());
|
||||
mLog.debug("CORR:" + errorMessage.toString());
|
||||
mLog.debug("ORIG:" + orig);
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package io.github.dsheirer.edac;
|
||||
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.bits.CorrectedBinaryMessage;
|
||||
|
||||
/*
|
||||
Adopted from https://github.com/boatbod/op25/blob/e5e8fe457240fb358a8163bcf6fe7dda5e4d8392/op25/gr-op25_repeater/lib/hamming.cc
|
||||
*/
|
||||
public class BPTC_17_12_3 {
|
||||
|
||||
public static boolean decode17123(BinaryMessage d, int offset)
|
||||
{
|
||||
if(d==null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate the checksum this column should have
|
||||
boolean c0 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(6 + offset) ^ d.get(7 + offset) ^ d.get(9 + offset);
|
||||
boolean c1 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(7 + offset) ^ d.get(8 + offset) ^ d.get(10 + offset);
|
||||
boolean c2 = d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(8 + offset) ^ d.get(9 + offset) ^ d.get(11 + offset);
|
||||
boolean c3 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(7 + offset) ^ d.get(10 + offset);
|
||||
boolean c4 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(5 + offset) ^ d.get(6 + offset) ^ d.get(8 + offset) ^ d.get(11 + offset);
|
||||
|
||||
// Compare these with the actual bits
|
||||
byte n = 0x00;
|
||||
n |= (c0 != d.get(12 + offset)) ? 0x01 : 0x00;
|
||||
n |= (c1 != d.get(13 + offset)) ? 0x02 : 0x00;
|
||||
n |= (c2 != d.get(14 + offset)) ? 0x04 : 0x00;
|
||||
n |= (c3 != d.get(15 + offset)) ? 0x08 : 0x00;
|
||||
n |= (c4 != d.get(16 + offset)) ? 0x10 : 0x00;
|
||||
|
||||
switch (n) {
|
||||
// Parity bit errors
|
||||
case 0x01: d.set(12 + offset, !d.get(12 + offset)); return true;
|
||||
case 0x02: d.set(13 + offset, !d.get(13 + offset)); return true;
|
||||
case 0x04: d.set(14 + offset, !d.get(14 + offset)); return true;
|
||||
case 0x08: d.set(15 + offset, !d.get(15 + offset)); return true;
|
||||
case 0x10: d.set(16 + offset, !d.get(16 + offset)); return true;
|
||||
|
||||
// Data bit errors
|
||||
case 0x1B: d.set(0 + offset, !d.get(0 + offset)); return true;
|
||||
case 0x1F: d.set(1 + offset, !d.get(1 + offset)); return true;
|
||||
case 0x17: d.set(2 + offset, !d.get(2 + offset)); return true;
|
||||
case 0x07: d.set(3 + offset, !d.get(3 + offset)); return true;
|
||||
case 0x0E: d.set(4 + offset, !d.get(4 + offset)); return true;
|
||||
case 0x1C: d.set(5 + offset, !d.get(5 + offset)); return true;
|
||||
case 0x11: d.set(6 + offset, !d.get(6 + offset)); return true;
|
||||
case 0x0B: d.set(7 + offset, !d.get(7 + offset)); return true;
|
||||
case 0x16: d.set(8 + offset, !d.get(8 + offset)); return true;
|
||||
case 0x05: d.set(9 + offset, !d.get(9 + offset)); return true;
|
||||
case 0x0A: d.set(10 + offset,!d.get(10 + offset)); return true;
|
||||
case 0x14: d.set(11 + offset,!d.get(11 + offset)); return true;
|
||||
|
||||
// No bit errors
|
||||
case 0x00: return true;
|
||||
|
||||
// Unrecoverable errors
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Block Product Turbo Code (BPTC) 196/96 Support
|
||||
*/
|
||||
public class BPTC_196_96
|
||||
{
|
||||
public static final int BPTC_LENGTH = 196;
|
||||
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
|
||||
public static final int CHECKSUM_COLUMN_COUNT = 4;
|
||||
public static final int MESSAGE_START_INDEX = 4;
|
||||
|
||||
/**
|
||||
* DMR De-interleaving indices
|
||||
*/
|
||||
public static int[] BPTC_DEINTERLEAVE = new int[]{0, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 16, 1, 182,
|
||||
167, 152, 137, 122, 107, 92, 77, 62, 47, 32, 17, 2, 183, 168, 153, 138, 123, 108, 93, 78, 63, 48, 33, 18, 3,
|
||||
184, 169, 154, 139, 124, 109, 94, 79, 64, 49, 34, 19, 4, 185, 170, 155, 140, 125, 110, 95, 80, 65, 50, 35, 20,
|
||||
5, 186, 171, 156, 141, 126, 111, 96, 81, 66, 51, 36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82, 67, 52, 37,
|
||||
22, 7, 188, 173, 158, 143, 128, 113, 98, 83, 68, 53, 38, 23, 8, 189, 174, 159, 144, 129, 114, 99, 84, 69, 54,
|
||||
39, 24, 9, 190, 175, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 191, 176, 161, 146, 131, 116, 101, 86, 71,
|
||||
56, 41, 26, 11, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 193, 178, 163, 148, 133, 118, 103,
|
||||
88, 73, 58, 43, 28, 13, 194, 179, 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 195, 180, 165, 150, 135, 120,
|
||||
105, 90, 75, 60, 45, 30, 15};
|
||||
|
||||
/**
|
||||
* DMR Column Indices
|
||||
*/
|
||||
public static int[][] COLUMN_INDEXES = new int[][]{
|
||||
{1, 16, 31, 46, 61, 76, 91, 106, 121, 136, 151, 166, 181},
|
||||
{2, 17, 32, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182},
|
||||
{3, 18, 33, 48, 63, 78, 93, 108, 123, 138, 153, 168, 183},
|
||||
{4, 19, 34, 49, 64, 79, 94, 109, 124, 139, 154, 169, 184},
|
||||
{5, 20, 35, 50, 65, 80, 95, 110, 125, 140, 155, 170, 185},
|
||||
{6, 21, 36, 51, 66, 81, 96, 111, 126, 141, 156, 171, 186},
|
||||
{7, 22, 37, 52, 67, 82, 97, 112, 127, 142, 157, 172, 187},
|
||||
{8, 23, 38, 53, 68, 83, 98, 113, 128, 143, 158, 173, 188},
|
||||
{9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 159, 174, 189},
|
||||
{10, 25, 40, 55, 70, 85, 100, 115, 130, 145, 160, 175, 190},
|
||||
{11, 26, 41, 56, 71, 86, 101, 116, 131, 146, 161, 176, 191},
|
||||
{12, 27, 42, 57, 72, 87, 102, 117, 132, 147, 162, 177, 192},
|
||||
{13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193},
|
||||
{14, 29, 44, 59, 74, 89, 104, 119, 134, 149, 164, 179, 194},
|
||||
{15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195}};
|
||||
|
||||
/**
|
||||
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
|
||||
* protected message
|
||||
* @param original uncorrected BPTC(196,96) message
|
||||
* @return error corrected and extracted 96-bit message
|
||||
*/
|
||||
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original)
|
||||
{
|
||||
//De-Interleave
|
||||
CorrectedBinaryMessage message = deinterleave(original);
|
||||
|
||||
//Perform error detection and correction
|
||||
correct(message);
|
||||
|
||||
//Extract the 96-bit message from the corrected 196 bits
|
||||
CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH);
|
||||
|
||||
//Transfer the corrected bit count
|
||||
extracted.setCorrectedBitCount(message.getCorrectedBitCount());
|
||||
|
||||
int index = MESSAGE_START_INDEX;
|
||||
|
||||
while(index < MAX_ORIGINAL_INDEX)
|
||||
{
|
||||
if((index % COLUMN_COUNT) < MESSAGE_COLUMN_COUNT)
|
||||
{
|
||||
try
|
||||
{
|
||||
extracted.add(message.get(index));
|
||||
}
|
||||
catch(BitSetFullException bsfe)
|
||||
{
|
||||
//Should never get to here
|
||||
bsfe.printStackTrace();
|
||||
return extracted;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
else
|
||||
{
|
||||
index += CHECKSUM_COLUMN_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
//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;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message
|
||||
* @param message that contains interleaved (transmitted) message
|
||||
* @return de-interleaved message
|
||||
*/
|
||||
private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage message)
|
||||
{
|
||||
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(BPTC_LENGTH);
|
||||
for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++)
|
||||
{
|
||||
deinterleaved.set(x, message.get(BPTC_DEINTERLEAVE[x]));
|
||||
}
|
||||
|
||||
return deinterleaved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the row level Hamming(15,11,3) protected words
|
||||
* @param message containing protected words in rows
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getRowErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> rowErrors = new ArrayList<>();
|
||||
|
||||
int offset = 0;
|
||||
int index = -1;
|
||||
|
||||
for(int row = 0; row < 13; row++)
|
||||
{
|
||||
//The first bit of the message is a pad/reserved bit and is ignored.
|
||||
offset = (row * 15) + 1;
|
||||
index = Hamming15.getErrorIndex(message, offset);
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
rowErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
//Note: row errors should already be in order and don't need sorted
|
||||
return rowErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the error indices from the column level Hamming(13,9,3) protected words
|
||||
* @param message containing protected words in columns
|
||||
* @return list of zero or more error indices
|
||||
*/
|
||||
private static List<Integer> getColumnErrors(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> columnErrors = new ArrayList<>();
|
||||
|
||||
int index = -1;
|
||||
|
||||
for(int column = 0; column < 15; column++)
|
||||
{
|
||||
index = Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
|
||||
|
||||
if(index >= 0)
|
||||
{
|
||||
columnErrors.add(index);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(columnErrors);
|
||||
return columnErrors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs error detection and correction on the DMR BPTC(196,96) protected message
|
||||
*
|
||||
* @param message to process and extract
|
||||
*/
|
||||
private static void correct(CorrectedBinaryMessage message)
|
||||
{
|
||||
List<Integer> rowErrors = getRowErrors(message);
|
||||
|
||||
if(!rowErrors.isEmpty())
|
||||
{
|
||||
List<Integer> columnErrors = getColumnErrors(message);
|
||||
|
||||
if(matches(rowErrors, columnErrors))
|
||||
{
|
||||
for(Integer index: rowErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
message.setCorrectedBitCount(rowErrors.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
//Step 1: correct row errors and recalculate column errors (should be zero)
|
||||
for(Integer index: rowErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
List<Integer> columnErrorsPart2 = getColumnErrors(message);
|
||||
|
||||
if(columnErrorsPart2.isEmpty())
|
||||
{
|
||||
//Success
|
||||
message.setCorrectedBitCount(rowErrors.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
//Reverse the row error correction
|
||||
for(Integer index: rowErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
//Step 2: correct column errors and recalculate row errors (should be zero)
|
||||
for(Integer index: columnErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
List<Integer> rowErrorsPart2 = getRowErrors(message);
|
||||
|
||||
if(rowErrorsPart2.isEmpty())
|
||||
{
|
||||
//Success
|
||||
message.setCorrectedBitCount(columnErrors.size());
|
||||
}
|
||||
else
|
||||
{
|
||||
//Reverse the column error correction
|
||||
for(Integer index: columnErrors)
|
||||
{
|
||||
message.flip(index);
|
||||
}
|
||||
|
||||
//Set the error count greater than the maximum correctable column errors (16)
|
||||
message.setCorrectedBitCount(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the errors detected by the row protected words match the errors detected by the column protected words
|
||||
* @param rowErrors bit positions
|
||||
* @param columnErrors bit positions
|
||||
* @return true if they have the same quantity of bit position errors and the bit positions match
|
||||
*/
|
||||
private static boolean matches(List<Integer> rowErrors, List<Integer> columnErrors)
|
||||
{
|
||||
if(rowErrors.size() == columnErrors.size())
|
||||
{
|
||||
for(int x = 0; x < rowErrors.size(); x++)
|
||||
{
|
||||
if(!rowErrors.get(x).equals(columnErrors.get(0)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String raw = "0000000001100010100101100011100100110100110101000100000011101001110001001000100001001010010100001000011111011000000011110000111000111111100011000011100001100000000101001010000100010111010100111001";
|
||||
System.out.println("RAW:" + raw);
|
||||
|
||||
CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.load(raw));
|
||||
System.out.println("LOD:" + message.toString());
|
||||
CorrectedBinaryMessage extracted = extract(message);
|
||||
System.out.println("EXT:" + extracted.toString());
|
||||
}
|
||||
}
|
||||
53
src/main/java/io/github/dsheirer/edac/CRC16.java
Normal file
53
src/main/java/io/github/dsheirer/edac/CRC16.java
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.IntField;
|
||||
|
||||
/**
|
||||
* Utility for calculating the CRC checksum for CRC-16 using polynomial 0x1021 and Initial Fill/Residual of 0xFFFF
|
||||
*/
|
||||
public class CRC16
|
||||
{
|
||||
/**
|
||||
* Calculates the 16-bit CRC checksum for the message using polynomial 0x1021 and residual 0xFFFF
|
||||
* @param message with transmitted 16-bit checksum at the end.
|
||||
* @return true if the check is correct or false if it fails the CRC check.
|
||||
*/
|
||||
public static boolean check(BinaryMessage message)
|
||||
{
|
||||
BinaryMessage copy = message.copy();
|
||||
BinaryMessage polynomial = BinaryMessage.loadHex("11021");
|
||||
polynomial.rotateLeft(3, 0, 20);
|
||||
polynomial.setSize(message.size());
|
||||
|
||||
int previousX = 0;
|
||||
for(int x = copy.nextSetBit(0); x >= 0 && x < copy.size() - 16; x = copy.nextSetBit(x + 1))
|
||||
{
|
||||
polynomial.rotateRight(x - previousX, previousX, 17 + x);
|
||||
previousX = x;
|
||||
copy.xor(polynomial);
|
||||
}
|
||||
|
||||
IntField crc = IntField.length16(copy.size() - 16);
|
||||
return copy.getInt(crc) == 0xFFFF;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 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
|
||||
@@ -44,9 +44,9 @@ public class Checksum_5_DMR
|
||||
/**
|
||||
* Indicates if the 77-bit Full Link Control message is valid.
|
||||
* @param message to check
|
||||
* @return true if the checksum matches.
|
||||
* @return residual value leftover from xor of the calculated and transmitted checksums.
|
||||
*/
|
||||
public static boolean isValid(CorrectedBinaryMessage message)
|
||||
public static int isValid(CorrectedBinaryMessage message)
|
||||
{
|
||||
int accumulator = message.getInt(LC_0);
|
||||
accumulator += message.getInt(LC_1);
|
||||
@@ -57,10 +57,8 @@ public class Checksum_5_DMR
|
||||
accumulator += message.getInt(LC_6);
|
||||
accumulator += message.getInt(LC_7);
|
||||
accumulator += message.getInt(LC_8);
|
||||
|
||||
accumulator = accumulator % 31;
|
||||
int checksum = message.getInt(CHECKSUM);
|
||||
|
||||
return accumulator == checksum;
|
||||
return accumulator ^ checksum;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 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
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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.CorrectedBinaryMessage;
|
||||
|
||||
/**
|
||||
* Implements Hamming(13,9,3) Error Detection algorithm
|
||||
@@ -8,8 +28,8 @@ import io.github.dsheirer.bits.BinaryMessage;
|
||||
public class Hamming13
|
||||
{
|
||||
//DMR Checksums from generator matrix TS 102 361-1 Table B.14
|
||||
private static int[] CHECKSUMS = new int[]{0xF, 0xE, 0x7, 0xA, 0x5, 0XB, 0xC, 0x6, 0x3};
|
||||
private static int[] ERROR_INDEX = new int[]{-1, 12, 11, 8, 10, 4, 7, 2, 9, -1, 3, 5, 6, -1, 1, 0, -1};
|
||||
private static int[] CHECKSUMS = new int[]{0xF, 0xE, 0x7, 0xA, 0x5, 0xB, 0xC, 0x6, 0x3, 0x8, 0x4, 0x2, 0x1};
|
||||
private static int[] ERROR_INDEX = new int[]{-1, 12, 11, 8, 10, 4, 7, 2, 9, -1, 3, 5, 6, -1, 1, 0, -1, -1};
|
||||
|
||||
/**
|
||||
* Calculates the bit error index of the Hamming(13,9,3) protected word that is contained in the binary message
|
||||
@@ -28,7 +48,7 @@ public class Hamming13
|
||||
return offset + ERROR_INDEX[syndrome];
|
||||
}
|
||||
|
||||
return -1;
|
||||
return IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,11 +73,11 @@ public class Hamming13
|
||||
}
|
||||
else
|
||||
{
|
||||
return 10000 + errorIndex;
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
return IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +115,7 @@ public class Hamming13
|
||||
return calculated;
|
||||
}
|
||||
|
||||
private static int getSyndrome(BinaryMessage message, int[] indices)
|
||||
public static int getSyndrome(BinaryMessage message, int[] indices)
|
||||
{
|
||||
int calculated = calculateChecksum(message, indices);
|
||||
|
||||
@@ -126,4 +146,16 @@ public class Hamming13
|
||||
int checksum = message.getInt(offset + 9, offset + 12);
|
||||
return (checksum ^ calculated);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String transmitted = "0000010000001";
|
||||
String correct = "0000000000000";
|
||||
String recommended = "0001000000001";
|
||||
// CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.load(transmitted));
|
||||
CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.load(correct));
|
||||
// CorrectedBinaryMessage cbm = new CorrectedBinaryMessage(CorrectedBinaryMessage.load(recommended));
|
||||
int syndrome = getSyndrome(cbm, 0);
|
||||
System.out.println("Syndrome " + syndrome);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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;
|
||||
@@ -8,7 +27,7 @@ import io.github.dsheirer.bits.BinaryMessage;
|
||||
public class Hamming15
|
||||
{
|
||||
//DMR Checksums from generator matrix TS 102 361-1 Table B.15
|
||||
private static int[] CHECKSUMS = new int[]{0x9, 0xD, 0xF, 0xE, 0x7, 0XA, 0x5, 0xB, 0xC, 0x6, 0x3};
|
||||
private static int[] CHECKSUMS = new int[]{0x9, 0xD, 0xF, 0xE, 0x7, 0xA, 0x5, 0xB, 0xC, 0x6, 0x3, 0x8, 0x4, 0x2, 0x1};
|
||||
private static int[] ERROR_INDEX = new int[]{-1, 14, 13, 10, 12, 6, 9, 4, 11, 0, 5, 7, 8, 1, 3, 2};
|
||||
|
||||
/**
|
||||
@@ -28,7 +47,7 @@ public class Hamming15
|
||||
return offset + ERROR_INDEX[syndrome];
|
||||
}
|
||||
|
||||
return -1;
|
||||
return IHamming.NO_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,7 +77,7 @@ public class Hamming15
|
||||
* @param offset to bit 0 of the hamming protected word
|
||||
* @return syndrome that can be used with the ERROR_INDEX error to find the index of the bit position error
|
||||
*/
|
||||
private static int getSyndrome(BinaryMessage message, int offset)
|
||||
public static int getSyndrome(BinaryMessage message, int offset)
|
||||
{
|
||||
int calculated = calculateChecksum(message, offset);
|
||||
int checksum = message.getInt(offset + 11, offset + 14);
|
||||
|
||||
@@ -1,13 +1,70 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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;
|
||||
|
||||
/**
|
||||
* Hamming(16,11,5) Error detection and correction utility.
|
||||
* Hamming(16,11,4) Error detection and correction utility. This code is capable of correcting single-bit errors and
|
||||
* detecting (but not correcting) double-bit errors.
|
||||
*/
|
||||
public class Hamming16
|
||||
public class Hamming16 implements IHamming
|
||||
{
|
||||
private static int[] CHECKSUMS = new int[]{0x13, 0x1A, 0x1F, 0x1C, 0x0E, 0x15, 0x0B, 0x16, 0x19, 0x0D, 0x07};
|
||||
private static int[] CHECKSUMS = new int[]{0x13, 0x1A, 0x1F, 0x1C, 0x0E, 0x15, 0x0B, 0x16, 0x19, 0x0D, 0x07, 0x10,
|
||||
0x08, 0x04, 0x02, 0x01};
|
||||
|
||||
/**
|
||||
* Calculates the bit error index of the Hamming(16,11,4) protected word that is contained in the binary message
|
||||
* starting at the specified offset.
|
||||
*
|
||||
* @param message containing a Hamming protected word
|
||||
* @param offset to the start of the protected word
|
||||
* @return message index for an error bit or -1 if no errors are detected or 1000 if two bit errors are detected.
|
||||
*/
|
||||
public int getErrorIndex(BinaryMessage message, int offset)
|
||||
{
|
||||
int syndrome = getSyndrome(message, offset);
|
||||
|
||||
if(syndrome == 0)
|
||||
{
|
||||
return NO_ERRORS;
|
||||
}
|
||||
|
||||
//If the syndrome indicates the error is in the final parity bit position, and we already have odd parity, then
|
||||
//flag it as invalid for multiple errors.
|
||||
if(syndrome == 1 && message.getSubMessage(offset, offset + 16).cardinality() % 2 == 1) //check final parity bit
|
||||
{
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(syndrome == CHECKSUMS[index])
|
||||
{
|
||||
return index + offset;
|
||||
}
|
||||
}
|
||||
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED)
|
||||
@@ -22,78 +79,36 @@ public class Hamming16
|
||||
{
|
||||
int syndrome = getSyndrome(frame, startIndex);
|
||||
|
||||
switch(syndrome)
|
||||
if (syndrome == 0)
|
||||
{
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
frame.flip(startIndex + 15); //Parity 1
|
||||
return 1;
|
||||
case 2:
|
||||
frame.flip(startIndex + 14); //Parity 2
|
||||
return 1;
|
||||
case 3:
|
||||
frame.flip(startIndex + 10); //Data 1
|
||||
return 1;
|
||||
case 4:
|
||||
frame.flip(startIndex + 13); //Parity 4
|
||||
return 1;
|
||||
case 5:
|
||||
frame.flip(startIndex + 9); //Data 2
|
||||
return 1;
|
||||
case 6:
|
||||
frame.flip(startIndex + 8); //Data 3
|
||||
return 1;
|
||||
case 7:
|
||||
frame.flip(startIndex + 7); //Data 4
|
||||
return 1;
|
||||
case 8:
|
||||
frame.flip(startIndex + 12); //Parity 8
|
||||
return 1;
|
||||
case 9:
|
||||
frame.flip(startIndex + 6); //Data 5
|
||||
return 1;
|
||||
case 10:
|
||||
frame.flip(startIndex + 5); //Data 6
|
||||
return 1;
|
||||
case 11:
|
||||
frame.flip(startIndex + 4); //Data 7
|
||||
return 1;
|
||||
case 12:
|
||||
frame.flip(startIndex + 3); //Data 8
|
||||
return 1;
|
||||
case 13:
|
||||
frame.flip(startIndex + 2); //Data 9
|
||||
return 1;
|
||||
case 14:
|
||||
frame.flip(startIndex + 1); //Data 10
|
||||
return 1;
|
||||
case 15:
|
||||
frame.flip(startIndex); //Data 11
|
||||
return 1;
|
||||
case 16:
|
||||
frame.flip(startIndex + 11); //Parity 16
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(CHECKSUMS[index] == syndrome)
|
||||
{
|
||||
frame.flip(startIndex + index);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the checksum (Parity 16,8,4,2,1) for data (11 <> 1 ) bits.
|
||||
* Calculates the checksum from the transmitted data bits.
|
||||
*
|
||||
* @param frame - frame containing hamming protected word
|
||||
* @param startIndex - start bit index of the hamming protected word
|
||||
* @return parity value, 0 - 16
|
||||
* @return calculated checksum value
|
||||
*/
|
||||
private static int calculateChecksum(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = 0; //Starting value
|
||||
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = frame.nextSetBit(startIndex);
|
||||
i >= startIndex && i < startIndex + 11;
|
||||
i = frame.nextSetBit(i + 1))
|
||||
for(int i = frame.nextSetBit(startIndex); i >= startIndex && i < startIndex + 11; i = frame.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CHECKSUMS[i - startIndex];
|
||||
}
|
||||
@@ -109,12 +124,10 @@ public class Hamming16
|
||||
* @param startIndex - of bit 0 of the hamming protected word
|
||||
* @return - 0 (no errors) or 1 (single bit error corrected)
|
||||
*/
|
||||
private static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
public static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = calculateChecksum(frame, startIndex);
|
||||
|
||||
int checksum = frame.getInt(startIndex + 11, startIndex + 15);
|
||||
|
||||
return (checksum ^ calculated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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;
|
||||
@@ -5,81 +24,44 @@ import io.github.dsheirer.bits.BinaryMessage;
|
||||
/**
|
||||
* Hamming(17,12,3) Error detection and correction utility.
|
||||
*/
|
||||
public class Hamming17
|
||||
public class Hamming17 implements IHamming
|
||||
{
|
||||
private static int[] CHECKSUMS = new int[]{0x1B, 0x1F, 0x1D, 0x1C, 0x0E, 0x07, 0x11, 0x1A, 0x0D, 0x14, 0x0A, 0x05};
|
||||
private static int[] CHECKSUMS = new int[]{0x1B, 0x1F, 0x1D, 0x1C, 0x0E, 0x07, 0x11, 0x1A, 0x0D, 0x14, 0x0A, 0x05,
|
||||
0x10, 0x08, 0x04, 0x02, 0x01};
|
||||
|
||||
/**
|
||||
* Performs error detection and correction of any single-bit errors and detection of any double-bit errors (SECDED)
|
||||
* Calculates the bit error index of the Hamming(17,12,3) protected word that is contained in the binary message
|
||||
* starting at the specified offset.
|
||||
*
|
||||
* @param frame - binary frame containing a hamming(17,12,5) protected field
|
||||
* @param startIndex - offset to the first bit of the field
|
||||
* @return - 0 = no errors
|
||||
* 1 = a single-bit error was detected and corrected
|
||||
* 2 = two or more errors detected - no corrections made
|
||||
* @param message containing a Hamming protected word
|
||||
* @param offset to the start of the protected word
|
||||
* @return message index for an error bit or -1 if no errors are detected or 1000 if more than one bit error detected.
|
||||
*/
|
||||
public static int checkAndCorrect(BinaryMessage frame, int startIndex)
|
||||
public int getErrorIndex(BinaryMessage message, int offset)
|
||||
{
|
||||
int syndrome = getSyndrome(frame, startIndex);
|
||||
int syndrome = getSyndrome(message, offset);
|
||||
|
||||
switch(syndrome)
|
||||
if(syndrome == 0)
|
||||
{
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
frame.flip(startIndex + 16); //Parity 1
|
||||
return 1;
|
||||
case 2:
|
||||
frame.flip(startIndex + 15); //Parity 2
|
||||
return 1;
|
||||
case 3:
|
||||
frame.flip(startIndex + 11); //Data 1
|
||||
return 1;
|
||||
case 4:
|
||||
frame.flip(startIndex + 14); //Parity 4
|
||||
return 1;
|
||||
case 5:
|
||||
frame.flip(startIndex + 10); //Data 2
|
||||
return 1;
|
||||
case 6:
|
||||
frame.flip(startIndex + 9); //Data 3
|
||||
return 1;
|
||||
case 7:
|
||||
frame.flip(startIndex + 8); //Data 4
|
||||
return 1;
|
||||
case 8:
|
||||
frame.flip(startIndex + 13); //Parity 8
|
||||
return 1;
|
||||
case 9:
|
||||
frame.flip(startIndex + 7); //Data 5
|
||||
return 1;
|
||||
case 10:
|
||||
frame.flip(startIndex + 6); //Data 6
|
||||
return 1;
|
||||
case 11:
|
||||
frame.flip(startIndex + 5); //Data 7
|
||||
return 1;
|
||||
case 12:
|
||||
frame.flip(startIndex + 4); //Data 8
|
||||
return 1;
|
||||
case 13:
|
||||
frame.flip(startIndex + 3); //Data 9
|
||||
return 1;
|
||||
case 14:
|
||||
frame.flip(startIndex + 2); //Data 10
|
||||
return 1;
|
||||
case 15:
|
||||
frame.flip(startIndex + 1); //Data 11
|
||||
return 1;
|
||||
case 16:
|
||||
frame.flip(startIndex + 12); //Parity 16
|
||||
return 1;
|
||||
case 17:
|
||||
frame.flip(startIndex); //Data 12
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
return NO_ERRORS;
|
||||
}
|
||||
|
||||
//If the syndrome indicates the error is in the final parity bit position, and we already have odd parity, then
|
||||
//flag it as invalid for multiple errors.
|
||||
if(syndrome == 1 && message.getSubMessage(offset, offset + 17).cardinality() % 2 == 1) //check final parity bit
|
||||
{
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
for(int index = 0; index < CHECKSUMS.length; index++)
|
||||
{
|
||||
if(syndrome == CHECKSUMS[index])
|
||||
{
|
||||
return index + offset;
|
||||
}
|
||||
}
|
||||
|
||||
return MULTIPLE_ERRORS;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,16 +69,14 @@ public class Hamming17
|
||||
*
|
||||
* @param frame - frame containing hamming protected word
|
||||
* @param startIndex - start bit index of the hamming protected word
|
||||
* @return parity value, 0 - 16
|
||||
* @return checksum value
|
||||
*/
|
||||
private static int calculateChecksum(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = 0; //Starting value
|
||||
|
||||
/* Iterate the set bits and XOR running checksum with lookup value */
|
||||
for(int i = frame.nextSetBit(startIndex);
|
||||
i >= startIndex && i < startIndex + 12;
|
||||
i = frame.nextSetBit(i + 1))
|
||||
for(int i = frame.nextSetBit(startIndex); i >= startIndex && i < startIndex + 12; i = frame.nextSetBit(i + 1))
|
||||
{
|
||||
calculated ^= CHECKSUMS[i - startIndex];
|
||||
}
|
||||
@@ -112,12 +92,10 @@ public class Hamming17
|
||||
* @param startIndex - of bit 0 of the hamming protected word
|
||||
* @return - 0 (no errors) or 1 (single bit error corrected)
|
||||
*/
|
||||
private static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
public static int getSyndrome(BinaryMessage frame, int startIndex)
|
||||
{
|
||||
int calculated = calculateChecksum(frame, startIndex);
|
||||
|
||||
int checksum = frame.getInt(startIndex + 12, startIndex + 16);
|
||||
|
||||
return (checksum ^ calculated);
|
||||
}
|
||||
}
|
||||
|
||||
39
src/main/java/io/github/dsheirer/edac/IHamming.java
Normal file
39
src/main/java/io/github/dsheirer/edac/IHamming.java
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 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;
|
||||
|
||||
/**
|
||||
* Interface for Hamming error index calculator.
|
||||
*/
|
||||
public interface IHamming
|
||||
{
|
||||
int NO_ERRORS = -1;
|
||||
int MULTIPLE_ERRORS = 1000;
|
||||
|
||||
/**
|
||||
* Calculate the error index for the Hamming protected binary sequence
|
||||
* @param message containing the binary sequence
|
||||
* @param offset to the start of the binary sequence
|
||||
* @return -1 if no errors, single-bit error index, or 1000 if there is more than a single-bit error detected.
|
||||
*/
|
||||
int getErrorIndex(BinaryMessage message, int offset);
|
||||
}
|
||||
535
src/main/java/io/github/dsheirer/edac/RS_12_9_DMR.java
Normal file
535
src/main/java/io/github/dsheirer/edac/RS_12_9_DMR.java
Normal file
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 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.CorrectedBinaryMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implements Reed Solomon RS(12,9,4) error detection and correction. The RS code is based on a shortened GF(8)
|
||||
* RS(255,252,4) code which can detect and correct up to 1x symbol error. Since each symbol is 8-bits long, we can
|
||||
* correct up to 8-bit errors, as long as the bit errors are contained within the same 8-bit symbol.
|
||||
*
|
||||
* The DMR full link control (FLC) message is composed of 96 bits representing 12x 8-bit symbols where the first
|
||||
* 9x symbols are message codewords and the 3x trailing are parity symbols.
|
||||
*
|
||||
* This class was ported from the original implementation at:
|
||||
* https://github.com/nonoo/dmrshark/blob/master/libs/coding/rs-12-9.c
|
||||
* License: dmrshark library uses the same GPL3 license used by sdrtrunk.
|
||||
*
|
||||
* Usage: all methods are static. Use the correct(CorrectedBinaryMessage cbm) method to detect and correct errors and
|
||||
* inspect the cbm.getCorrectedBitCount(). A non-negative value indicates success and the count of bits corrected.
|
||||
*/
|
||||
public class RS_12_9_DMR
|
||||
{
|
||||
/**
|
||||
* Primitive polynomial used to generate the EXPONENTS_TABLE and LOG_TABLE values. See p.142, B.14
|
||||
*/
|
||||
private static final int PRIMITIVE_POLYNOMIAL = 0x11D;
|
||||
|
||||
// See DMR AI. spec. page 142, Table B.19
|
||||
private static final int[] EXPONENTS_TABLE = {
|
||||
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1D, 0x3A, 0x74, 0xE8, 0xCD, 0x87, 0x13, 0x26,
|
||||
0x4C, 0x98, 0x2D, 0x5A, 0xB4, 0x75, 0xEA, 0xC9, 0x8F, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0,
|
||||
0x9D, 0x27, 0x4E, 0x9C, 0x25, 0x4A, 0x94, 0x35, 0x6A, 0xD4, 0xB5, 0x77, 0xEE, 0xC1, 0x9F, 0x23,
|
||||
0x46, 0x8C, 0x05, 0x0A, 0x14, 0x28, 0x50, 0xA0, 0x5D, 0xBA, 0x69, 0xD2, 0xB9, 0x6F, 0xDE, 0xA1,
|
||||
0x5F, 0xBE, 0x61, 0xC2, 0x99, 0x2F, 0x5E, 0xBC, 0x65, 0xCA, 0x89, 0x0F, 0x1E, 0x3C, 0x78, 0xF0,
|
||||
0xFD, 0xE7, 0xD3, 0xBB, 0x6B, 0xD6, 0xB1, 0x7F, 0xFE, 0xE1, 0xDF, 0xA3, 0x5B, 0xB6, 0x71, 0xE2,
|
||||
0xD9, 0xAF, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0D, 0x1A, 0x34, 0x68, 0xD0, 0xBD, 0x67, 0xCE,
|
||||
0x81, 0x1F, 0x3E, 0x7C, 0xF8, 0xED, 0xC7, 0x93, 0x3B, 0x76, 0xEC, 0xC5, 0x97, 0x33, 0x66, 0xCC,
|
||||
0x85, 0x17, 0x2E, 0x5C, 0xB8, 0x6D, 0xDA, 0xA9, 0x4F, 0x9E, 0x21, 0x42, 0x84, 0x15, 0x2A, 0x54,
|
||||
0xA8, 0x4D, 0x9A, 0x29, 0x52, 0xA4, 0x55, 0xAA, 0x49, 0x92, 0x39, 0x72, 0xE4, 0xD5, 0xB7, 0x73,
|
||||
0xE6, 0xD1, 0xBF, 0x63, 0xC6, 0x91, 0x3F, 0x7E, 0xFC, 0xE5, 0xD7, 0xB3, 0x7B, 0xF6, 0xF1, 0xFF,
|
||||
0xE3, 0xDB, 0xAB, 0x4B, 0x96, 0x31, 0x62, 0xC4, 0x95, 0x37, 0x6E, 0xDC, 0xA5, 0x57, 0xAE, 0x41,
|
||||
0x82, 0x19, 0x32, 0x64, 0xC8, 0x8D, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0xE0, 0xDD, 0xA7, 0x53, 0xA6,
|
||||
0x51, 0xA2, 0x59, 0xB2, 0x79, 0xF2, 0xF9, 0xEF, 0xC3, 0x9B, 0x2B, 0x56, 0xAC, 0x45, 0x8A, 0x09,
|
||||
0x12, 0x24, 0x48, 0x90, 0x3D, 0x7A, 0xF4, 0xF5, 0xF7, 0xF3, 0xFB, 0xEB, 0xCB, 0x8B, 0x0B, 0x16,
|
||||
0x2C, 0x58, 0xB0, 0x7D, 0xFA, 0xE9, 0xCF, 0x83, 0x1B, 0x36, 0x6C, 0xD8, 0xAD, 0x47, 0x8E, 0x01
|
||||
};
|
||||
|
||||
// See DMR AI. spec. page 143, Table B.20.
|
||||
private static final int[] LOG_TABLE = {0, 0, 1, 25, 2, 50, 26, 198, 3, 223, 51, 238, 27, 104, 199, 75, 4, 100, 224,
|
||||
14, 52, 141, 239, 129, 28, 193, 105, 248, 200, 8, 76, 113, 5, 138, 101, 47, 225, 36, 15, 33, 53, 147, 142, 218,
|
||||
240, 18, 130, 69, 29, 181, 194, 125, 106, 39, 249, 185, 201, 154, 9, 120, 77, 228, 114, 166, 6, 191, 139, 98,
|
||||
102, 221, 48, 253, 226, 152, 37, 179, 16, 145, 34, 136, 54, 208, 148, 206, 143, 150, 219, 189, 241, 210, 19, 92,
|
||||
131, 56, 70, 64, 30, 66, 182, 163, 195, 72, 126, 110, 107, 58, 40, 84, 250, 133, 186, 61, 202, 94, 155, 159, 10,
|
||||
21, 121, 43, 78, 212, 229, 172, 115, 243, 167, 87, 7, 112, 192, 247, 140, 128, 99, 13, 103, 74, 222, 237, 49,
|
||||
197, 254, 24, 227, 165, 153, 119, 38, 184, 180, 124, 17, 68, 146, 217, 35, 32, 137, 46, 55, 63, 209, 91, 149,
|
||||
188, 207, 205, 144, 135, 151, 178, 220, 252, 190, 97, 242, 86, 211, 171, 20, 42, 93, 158, 132, 60, 57, 83, 71,
|
||||
109, 65, 162, 31, 45, 67, 216, 183, 123, 164, 118, 196, 23, 73, 236, 127, 12, 111, 246, 108, 161, 59, 82, 41,
|
||||
157, 85, 170, 251, 96, 134, 177, 187, 204, 62, 90, 203, 89, 95, 176, 156, 169, 160, 81, 11, 245, 22, 235, 122,
|
||||
117, 44, 215, 79, 174, 213, 233, 230, 231, 173, 232, 116, 214, 244, 234, 168, 80, 88, 175
|
||||
};
|
||||
|
||||
// See DMR AI. spec. page 141, B.10 for these coefficients.
|
||||
private static final int[] GENERATOR_POLYNOMIAL = {0x40, 0x38, 0x0e, 0x01};
|
||||
private static final int CODEWORD_SIZE = 12;
|
||||
private static final int CHECKSUM_SIZE = 3; //L
|
||||
private static final int POLYNOMIAL_MAXIMUM_DEGREE = (CHECKSUM_SIZE * 2);
|
||||
public static final int ERRORS_CANT_BE_CORRECTED = -1;
|
||||
|
||||
/**
|
||||
* Performs error detection and correction on the Full Link Control (FLC) message. The message should be 96-bits
|
||||
* long containing 9x 8-bit message symbols and 3x 8-bit parity symbols. The RS(12,9,4) algorithm can detect and
|
||||
* correct up to 1x 8-bit symbol, or up to 8 bit errors if they are all contained in the same symbol.
|
||||
*
|
||||
* Error detection and correction results will be stored in the message.getCorrectedBitCount() and the message will
|
||||
* be corrected in place, if able to correct. A corrected bit count of ERRORS_CANT_BE_CORRECTED (-1) indicates that
|
||||
* the message could not be corrected.
|
||||
*
|
||||
* @param cbm to correct.
|
||||
* @param mask to apply/remove before error detection and correction.
|
||||
*/
|
||||
public static int correct(CorrectedBinaryMessage cbm, int mask)
|
||||
{
|
||||
int[] codeword = new int[12];
|
||||
|
||||
for(int index = 0; index < 12; index++)
|
||||
{
|
||||
codeword[index] = (0xFF & cbm.getByte(index * 8));
|
||||
|
||||
//Apply the mask to parity symbols 9-11
|
||||
if(index >= 9)
|
||||
{
|
||||
codeword[index] ^= (mask & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
int[] syndrome = calculateSyndrome(codeword);
|
||||
|
||||
if(hasErrors(syndrome))
|
||||
{
|
||||
int correctedSymbolIndex = correctErrors(codeword, syndrome);
|
||||
|
||||
if(correctedSymbolIndex >= 0)
|
||||
{
|
||||
//Get the previous symbol value and xor to calculate the bit error correction pattern
|
||||
int previousValue = (0xFF & cbm.getByte(correctedSymbolIndex * 8));
|
||||
int correctionPattern = previousValue ^ codeword[correctedSymbolIndex];
|
||||
|
||||
//Apply the correction to the original message
|
||||
cbm.setByte(8 * correctedSymbolIndex, (byte)(0xFF & codeword[correctedSymbolIndex]));
|
||||
|
||||
//Set the corrected bit count to the number of ones in the correction pattern from the corrected symbol.
|
||||
cbm.setCorrectedBitCount(Integer.bitCount(correctionPattern));
|
||||
return 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Message can't be corrected - too many errors.
|
||||
cbm.setCorrectedBitCount(correctedSymbolIndex);
|
||||
|
||||
//Calculate the checksum residual and return that as the observed alternate mask value. Note: the
|
||||
//DMR ICD checksum has already been applied to the codeword and this calculated residual is the
|
||||
//'other' masking value that is being used on both Headers and Terminators.
|
||||
int[] checksum = calculateChecksum(codeword);
|
||||
|
||||
for(int index = 0; index < 3; index++)
|
||||
{
|
||||
checksum[index] ^= codeword[index + 9];
|
||||
}
|
||||
|
||||
return checksum[0] << 16 | checksum[1] << 8 | checksum[2];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cbm.setCorrectedBitCount(0);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs GF(8) multiplication (a + b % 255)
|
||||
* @param factorA factor
|
||||
* @param factorB factor
|
||||
* @return product
|
||||
*/
|
||||
private static int galoisMultiplication(int factorA, int factorB)
|
||||
{
|
||||
if (factorA == 0 || factorB == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return EXPONENTS_TABLE[((LOG_TABLE[factorA & 0xFF] + LOG_TABLE[factorB & 0xFF]) % 255)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the inverse.
|
||||
* @param elt value
|
||||
* @return inverse
|
||||
*/
|
||||
private static int galoisInverse(int elt)
|
||||
{
|
||||
return EXPONENTS_TABLE[255 - LOG_TABLE[elt]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the polynomial by z, resulting in a shift right by 1 operation.
|
||||
* @param poly to multiply.
|
||||
*/
|
||||
private static void multiplyPolynomialByZ(int[] poly)
|
||||
{
|
||||
for(int i = POLYNOMIAL_MAXIMUM_DEGREE - 1; i > 0; i--)
|
||||
{
|
||||
poly[i] = poly[i - 1];
|
||||
}
|
||||
|
||||
poly[0] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the polynomials and returns the result.
|
||||
* @param poly1 argument
|
||||
* @param poly2 argument
|
||||
* @return result
|
||||
*/
|
||||
private static int[] multiplyPolynomials(int[] poly1, int[] poly2)
|
||||
{
|
||||
int[] product = new int[POLYNOMIAL_MAXIMUM_DEGREE * 2];
|
||||
|
||||
int i;
|
||||
int j;
|
||||
int[] tmp1 = new int[POLYNOMIAL_MAXIMUM_DEGREE * 2];
|
||||
|
||||
for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE * 2; i++)
|
||||
{
|
||||
product[i] = 0;
|
||||
}
|
||||
|
||||
for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++)
|
||||
{
|
||||
for(j = POLYNOMIAL_MAXIMUM_DEGREE; j < (POLYNOMIAL_MAXIMUM_DEGREE *2); j++)
|
||||
{
|
||||
tmp1[j] = 0;
|
||||
}
|
||||
|
||||
// Scale tmp1 by p1[i]
|
||||
for(j = 0; j < POLYNOMIAL_MAXIMUM_DEGREE; j++)
|
||||
{
|
||||
tmp1[j] = galoisMultiplication(poly2[j], poly1[i]);
|
||||
}
|
||||
|
||||
// Shift (multiply) tmp1 right by i
|
||||
for (j = (POLYNOMIAL_MAXIMUM_DEGREE * 2) - 1; j >= i; j--)
|
||||
{
|
||||
tmp1[j] = tmp1[j-i];
|
||||
}
|
||||
|
||||
for (j = 0; j < i; j++)
|
||||
{
|
||||
tmp1[j] = 0;
|
||||
}
|
||||
|
||||
// Add into partial product.
|
||||
for (j = 0; j < (POLYNOMIAL_MAXIMUM_DEGREE *2); j++)
|
||||
{
|
||||
product[j] ^= tmp1[j];
|
||||
}
|
||||
}
|
||||
|
||||
return product;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the error evaluator polynomial (EEP). Computes the combined erasure/error evaluator polynomial
|
||||
* (error_locator_poly*syndrome mod z^4
|
||||
* @param elp error locator polynomial
|
||||
* @param syndrome calculated from the codeword
|
||||
* @return error evaluator polynomial (EEP)
|
||||
*/
|
||||
private static int[] calculateEEP(int[] elp, int[] syndrome)
|
||||
{
|
||||
int[] eep = new int[POLYNOMIAL_MAXIMUM_DEGREE];
|
||||
int[] product = multiplyPolynomials(elp, syndrome);
|
||||
int i;
|
||||
|
||||
for (i = 0; i < CHECKSUM_SIZE; i++)
|
||||
{
|
||||
eep[i] = product[i];
|
||||
}
|
||||
|
||||
for (; i < POLYNOMIAL_MAXIMUM_DEGREE; i++)
|
||||
{
|
||||
eep[i] = 0;
|
||||
}
|
||||
|
||||
return eep;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the discrepancy.
|
||||
* @param elp error locator polynomial
|
||||
* @param syndrome from the codeword
|
||||
* @param L maximum capacity to correct.
|
||||
* @param n argument
|
||||
* @return discrepancy
|
||||
*/
|
||||
private static int computeDiscrepancy(int[] elp, int[] syndrome, int L, int n)
|
||||
{
|
||||
int sum = 0;
|
||||
|
||||
for (int i = 0; i <= L; i++)
|
||||
{
|
||||
sum ^= galoisMultiplication(elp[i], syndrome[n - i]);
|
||||
}
|
||||
|
||||
return sum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the coefficients of the error locator polynomial (ELP) and then calculates the error evaluator
|
||||
* (EEP) using the Berlekamp-Massey algorithm. From Cain, Clark, "Error-Correction Coding For Digital
|
||||
* Communications", pp. 216.
|
||||
* @param syndrome calculated from the codeword
|
||||
* @param elp calculated.
|
||||
* @return EEP
|
||||
*/
|
||||
private static int[] calculateELP(int[] syndrome)
|
||||
{
|
||||
int[] elp = new int[POLYNOMIAL_MAXIMUM_DEGREE];
|
||||
int[] psi2 = new int[POLYNOMIAL_MAXIMUM_DEGREE];
|
||||
int[] bigD = new int[POLYNOMIAL_MAXIMUM_DEGREE];
|
||||
|
||||
int L = 0;
|
||||
int L2;
|
||||
int k = -1;
|
||||
int d;
|
||||
int i;
|
||||
|
||||
bigD[1] = 1;
|
||||
elp[0] = 1;
|
||||
|
||||
for(int n = 0; n < CHECKSUM_SIZE; n++)
|
||||
{
|
||||
d = computeDiscrepancy(elp, syndrome, L, n);
|
||||
|
||||
if (d != 0)
|
||||
{
|
||||
// psi2 = error_locator_poly - d*D
|
||||
for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++)
|
||||
{
|
||||
psi2[i] = (elp[i] ^ galoisMultiplication(d, bigD[i]));
|
||||
}
|
||||
|
||||
if (L < (n - k))
|
||||
{
|
||||
L2 = (n - k);
|
||||
k = (n - L);
|
||||
|
||||
for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++)
|
||||
{
|
||||
bigD[i] = galoisMultiplication(elp[i], galoisInverse(d));
|
||||
}
|
||||
|
||||
L = L2;
|
||||
}
|
||||
|
||||
// error_locator_poly = psi2
|
||||
for (i = 0; i < POLYNOMIAL_MAXIMUM_DEGREE; i++)
|
||||
{
|
||||
elp[i] = psi2[i];
|
||||
}
|
||||
}
|
||||
|
||||
multiplyPolynomialByZ(bigD);
|
||||
}
|
||||
|
||||
return elp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The error-locator polynomial's roots are found by looking for the values of a^n where evaluating the polynomial
|
||||
* yields zero (evaluating rs_12_9_error_locator_poly at successive values of alpha (Chien's search)).
|
||||
* @param elp error locator polynomial.
|
||||
* @return error location roots.
|
||||
*/
|
||||
private static List<Integer> findRoots(int[] elp)
|
||||
{
|
||||
List<Integer> roots = new ArrayList<>();
|
||||
int sum;
|
||||
int k;
|
||||
|
||||
for(int r = 1; r < 256; r++)
|
||||
{
|
||||
sum = 0;
|
||||
|
||||
// Evaluate ELP at r
|
||||
for(k = 0; k < CHECKSUM_SIZE + 1; k++)
|
||||
{
|
||||
sum ^= galoisMultiplication(EXPONENTS_TABLE[(k * r) % 255], elp[k]);
|
||||
}
|
||||
|
||||
if(sum == 0)
|
||||
{
|
||||
roots.add(255 - r);
|
||||
}
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the syndrome for a codeword.
|
||||
* @param codeword to check.
|
||||
* @return syndrome.
|
||||
*/
|
||||
private static int[] calculateSyndrome(int[] codeword)
|
||||
{
|
||||
if(codeword.length != CODEWORD_SIZE)
|
||||
{
|
||||
throw new IllegalArgumentException("Codeword length [" + codeword.length + "] should be " + CODEWORD_SIZE +
|
||||
" symbols");
|
||||
}
|
||||
|
||||
int[] syndrome = new int[POLYNOMIAL_MAXIMUM_DEGREE];
|
||||
|
||||
int i, temp;
|
||||
|
||||
for(int j = 0; j < 3; j++)
|
||||
{
|
||||
for (i = 0; i < codeword.length; i++)
|
||||
{
|
||||
temp = galoisMultiplication(EXPONENTS_TABLE[j + 1], syndrome[j]);
|
||||
temp ^= codeword[i];
|
||||
syndrome[j] = (0xFF & temp);
|
||||
}
|
||||
}
|
||||
|
||||
return syndrome;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the syndrome array for non-zero values indicating an error.
|
||||
* @param syndrome to check
|
||||
* @return true if the syndrome array has any non-zero values.
|
||||
*/
|
||||
private static boolean hasErrors(int[] syndrome)
|
||||
{
|
||||
for(int symbol: syndrome)
|
||||
{
|
||||
if(symbol != 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Corrects errors in the codeword using the calculated syndrome.
|
||||
* @param codeword to correct. The codeword will be modified with corrections, if able.
|
||||
* @param syndrome calculated from the codeword.
|
||||
* @return -1 if the codeword can't be corrected or the index of the symbol that was corrected.
|
||||
*/
|
||||
private static int correctErrors(int[] codeword, int[] syndrome)
|
||||
{
|
||||
int r;
|
||||
int i;
|
||||
int j;
|
||||
int errorMask;
|
||||
int num, denom;
|
||||
|
||||
//Error Locator Polynomial (ELP)
|
||||
int[] elp = calculateELP(syndrome);
|
||||
|
||||
//Error Evaluator Polynomial (EEP)
|
||||
int[] eep = calculateEEP(elp, syndrome);
|
||||
|
||||
List<Integer> roots = findRoots(elp);
|
||||
|
||||
if (roots.isEmpty())
|
||||
{
|
||||
return ERRORS_CANT_BE_CORRECTED;
|
||||
}
|
||||
|
||||
// Error correction is done using the error-evaluator equation on pp 207.
|
||||
if (roots.size() <= CHECKSUM_SIZE)
|
||||
{
|
||||
// First check for illegal error locations.
|
||||
for (r = 0; r < roots.size(); r++)
|
||||
{
|
||||
if (roots.get(r) >= CODEWORD_SIZE)
|
||||
{
|
||||
return ERRORS_CANT_BE_CORRECTED;
|
||||
}
|
||||
}
|
||||
|
||||
int errorsCorrected = 0;
|
||||
|
||||
// Evaluates rs_12_9_error_evaluator_poly/rs_12_9_error_locator_poly' at the roots
|
||||
// alpha^(-i) for error locs i.
|
||||
for (r = 0; r < roots.size(); r++)
|
||||
{
|
||||
i = roots.get(r);
|
||||
|
||||
// Evaluate rs_12_9_error_evaluator_poly at alpha^(-i)
|
||||
num = 0;
|
||||
for (j = 0; j < POLYNOMIAL_MAXIMUM_DEGREE; j++)
|
||||
{
|
||||
num ^= galoisMultiplication(eep[j], EXPONENTS_TABLE[((255 - i) * j) % 255]);
|
||||
}
|
||||
|
||||
// Evaluate rs_12_9_error_evaluator_poly' (derivative) at alpha^(-i). All odd powers disappear.
|
||||
denom = 0;
|
||||
|
||||
for (j = 1; j < POLYNOMIAL_MAXIMUM_DEGREE; j += 2)
|
||||
{
|
||||
denom ^= galoisMultiplication(elp[j], EXPONENTS_TABLE[((255-i)*(j-1)) % 255]);
|
||||
}
|
||||
|
||||
errorMask = galoisMultiplication(num, galoisInverse(denom));
|
||||
int index = CODEWORD_SIZE - i - 1;
|
||||
codeword[index] ^= errorMask;
|
||||
return index;
|
||||
}
|
||||
|
||||
return errorsCorrected;
|
||||
}
|
||||
|
||||
return ERRORS_CANT_BE_CORRECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the checksum parity symbols for the codeword using the DMR generator polynomial.
|
||||
* @param codeword to calculate.
|
||||
* @return checksum bytes (3)
|
||||
*/
|
||||
private static int[] calculateChecksum(int[] codeword)
|
||||
{
|
||||
int[] checksum = new int[CHECKSUM_SIZE];
|
||||
|
||||
int i;
|
||||
int feedback;
|
||||
|
||||
for (i = 0; i < 9; i++)
|
||||
{
|
||||
feedback = (codeword[i] ^ checksum[0]);
|
||||
checksum[0] = (checksum[1] ^ galoisMultiplication(GENERATOR_POLYNOMIAL[2], feedback));
|
||||
checksum[1] = (checksum[2] ^ galoisMultiplication(GENERATOR_POLYNOMIAL[1], feedback));
|
||||
checksum[2] = galoisMultiplication(GENERATOR_POLYNOMIAL[0], feedback);
|
||||
}
|
||||
|
||||
return checksum;
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.CorrectedBinaryMessage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Berlecamp-Massey decoder for Reed Solomon RS(12,9,4) protected messages using DMR generator polynomial
|
||||
*/
|
||||
public class ReedSolomon_12_9_4_DMR extends ReedSolomon_255_DMR
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ReedSolomon_12_9_4_DMR.class);
|
||||
|
||||
private static final int[] FLC_CODEWORD_0 = {0, 1, 2, 3, 4, 5, 6, 7};
|
||||
private static final int[] FLC_CODEWORD_1 = {8, 9, 10, 11, 12, 13, 14, 15};
|
||||
private static final int[] FLC_CODEWORD_2 = {16, 17, 18, 19, 20, 21, 22, 23};
|
||||
private static final int[] FLC_CODEWORD_3 = {24, 25, 26, 27, 28, 29, 30, 31};
|
||||
private static final int[] FLC_CODEWORD_4 = {32, 33, 34, 35, 36, 37, 38, 39};
|
||||
private static final int[] FLC_CODEWORD_5 = {40, 41, 42, 43, 44, 45, 46, 47};
|
||||
private static final int[] FLC_CODEWORD_6 = {48, 49, 50, 51, 52, 53, 54, 55};
|
||||
private static final int[] FLC_CODEWORD_7 = {56, 57, 58, 59, 60, 61, 62, 63};
|
||||
private static final int[] FLC_CODEWORD_8 = {64, 65, 66, 67, 68, 69, 70, 71};
|
||||
private static final int[] FLC_PARITY_0 = {72, 73, 74, 75, 76, 77, 78, 79};
|
||||
private static final int[] FLC_PARITY_1 = {80, 81, 82, 83, 84, 85, 86, 87};
|
||||
private static final int[] FLC_PARITY_2 = {88, 89, 90, 91, 92, 93, 94, 95};
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public ReedSolomon_12_9_4_DMR()
|
||||
{
|
||||
//Note: the RS(12,9,4) is a shortened form of RS(255,252,4) over GF(8)
|
||||
super(255, 252);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to correct the Full Link Control message returning true if there were zero or one correctable errors
|
||||
* and returning false if there were two or more errors detected.
|
||||
*
|
||||
* Note: if one bit error was detected and corrected, the corrected bit count is set to 1 in the message
|
||||
*
|
||||
* @param message to be corrected.
|
||||
* @param crcMask as defined in the TS 102-361-1 Table B.21
|
||||
* @return true if the message is correct or if the message was corrected.
|
||||
*/
|
||||
public boolean correctFullLinkControl(CorrectedBinaryMessage message, int crcMask)
|
||||
{
|
||||
int[] input = new int[255];
|
||||
int[] output = new int[255];
|
||||
|
||||
input[0] = (message.getInt(FLC_PARITY_2) ^ crcMask);
|
||||
input[1] = (message.getInt(FLC_PARITY_1) ^ crcMask);
|
||||
input[2] = (message.getInt(FLC_PARITY_0) ^ crcMask);
|
||||
input[3] = message.getInt(FLC_CODEWORD_8);
|
||||
input[4] = message.getInt(FLC_CODEWORD_7);
|
||||
input[5] = message.getInt(FLC_CODEWORD_6);
|
||||
input[6] = message.getInt(FLC_CODEWORD_5);
|
||||
input[7] = message.getInt(FLC_CODEWORD_4);
|
||||
input[8] = message.getInt(FLC_CODEWORD_3);
|
||||
input[9] = message.getInt(FLC_CODEWORD_2);
|
||||
input[10] = message.getInt(FLC_CODEWORD_1);
|
||||
input[11] = message.getInt(FLC_CODEWORD_0);
|
||||
|
||||
try
|
||||
{
|
||||
boolean irrecoverableErrors = decode(input, output);
|
||||
|
||||
if(irrecoverableErrors)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int pointer = 0;
|
||||
|
||||
for(int x = 11; x >= 0; x--)
|
||||
{
|
||||
if(output[x] != -1 && (output[x] != input[x]))
|
||||
{
|
||||
message.load(pointer, 8, output[x]);
|
||||
message.setCorrectedBitCount(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
pointer += 8;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
int[] output = new int[255];
|
||||
int[] input = new int[255];
|
||||
input[0] = 247;
|
||||
input[1] = 236;
|
||||
input[2] = 171;
|
||||
input[3] = 24;
|
||||
input[4] = 46;
|
||||
input[5] = 177;
|
||||
input[6] = 246;
|
||||
input[7] = 205;
|
||||
input[8] = 232;
|
||||
input[9] = 32;
|
||||
input[10] = 124;
|
||||
input[11] = 0;
|
||||
|
||||
// input[254] = 247;
|
||||
// input[253] = 236;
|
||||
// input[252] = 171;
|
||||
// input[251] = 24;
|
||||
// input[250] = 46;
|
||||
// input[249] = 177;
|
||||
// input[248] = 246;
|
||||
// input[247] = 205;
|
||||
// input[246] = 232;
|
||||
// input[245] = 32;
|
||||
// input[244] = 124;
|
||||
// input[243] = 0;
|
||||
|
||||
ReedSolomon_12_9_4_DMR rs = new ReedSolomon_12_9_4_DMR();
|
||||
boolean irrecoverable = rs.decode(input, output);
|
||||
|
||||
mLog.info("Irrecoverable: " + irrecoverable);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -27,7 +27,7 @@ public class ReedSolomon_63_P25 extends BerlekempMassey
|
||||
/**
|
||||
* APCO-25 Reed-Solomon code is generated from a Galois Field(2^6) by the polynomial: a6 + a1 + a0.
|
||||
* In binary, this is expressed as: 1000011 which is reversed to big-endian format for this algorithm
|
||||
* See: TIA 102-BAAA paragraph 5.9 Reed-Solomon Code Generator Matrices
|
||||
* See: TIA 102-BAAA paragraph 4.9 Reed-Solomon Code Generator Matrices
|
||||
*/
|
||||
public static final int[] P25_GENERATOR_POLYNOMIAL = { 1, 1, 0, 0, 0, 0, 1 };
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user