mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-14 01:27:34 -05:00
Compare commits
65 Commits
nightly
...
2466-gradl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
395a9682da | ||
|
|
7b249af944 | ||
|
|
197cec04f3 | ||
|
|
c75baa48f4 | ||
|
|
866aadc8c3 | ||
|
|
276f4a316a | ||
|
|
e65ff5a8dc | ||
|
|
85bfd21257 | ||
|
|
9ebbf957a2 | ||
|
|
00c0418017 | ||
|
|
33a2471338 | ||
|
|
d86fba6945 | ||
|
|
248fe3e4f7 | ||
|
|
9a6dc2b0a0 | ||
|
|
6b9cd2d317 | ||
|
|
f2c98cdb40 | ||
|
|
5635341d12 | ||
|
|
df739f525f | ||
|
|
c8b1c8ed3a | ||
|
|
b0428a1d8b | ||
|
|
c0a96f94fb | ||
|
|
1945b83562 | ||
|
|
dc550f3121 | ||
|
|
0460f2b77b | ||
|
|
b37ed51d07 | ||
|
|
f1183e531d | ||
|
|
16a66112fc | ||
|
|
8aa6ae089a | ||
|
|
728a83a82d | ||
|
|
c05328cd1e | ||
|
|
a9120ed65e | ||
|
|
9305abc784 | ||
|
|
0c637ed80d | ||
|
|
7ee3c5d251 | ||
|
|
a33c99347b | ||
|
|
97cae50bdd | ||
|
|
80360029ef | ||
|
|
af5dbcfcdc | ||
|
|
9dcebb4943 | ||
|
|
e2d9c4cda3 | ||
|
|
3e21887d27 | ||
|
|
d60720aab7 | ||
|
|
2292af6c07 | ||
|
|
ff128f56d2 | ||
|
|
3c01c644c9 | ||
|
|
ade4dc47cd | ||
|
|
cbbbe3a131 | ||
|
|
b385a42b63 | ||
|
|
ccae1d05e7 | ||
|
|
788d994ad7 | ||
|
|
7cf6974b6b | ||
|
|
cf0737ed90 | ||
|
|
75fa495c99 | ||
|
|
7945f0ce63 | ||
|
|
a053315675 | ||
|
|
de0e722f0c | ||
|
|
a42592daa9 | ||
|
|
7f9dc86b23 | ||
|
|
2f5c165aec | ||
|
|
29164ac75d | ||
|
|
185df1735a | ||
|
|
c81e6cccfa | ||
|
|
a2509554db | ||
|
|
b83f77c7c6 | ||
|
|
d8e2e0ac57 |
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 25
|
||||
- name: Set up JDK 27
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '25'
|
||||
java-version: '27'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
|
||||
14
.github/workflows/nightly.yml
vendored
14
.github/workflows/nightly.yml
vendored
@@ -15,10 +15,11 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 25
|
||||
- name: Set up JDK 27
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '25'
|
||||
# Must match the JDK version downloaded in build.gradle (jdk_download_base) - jlink requires an exact version match
|
||||
java-version: '27.0.0'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
|
||||
@@ -37,11 +38,12 @@ jobs:
|
||||
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
|
||||
|
||||
- name: Add build artifacts to nightly release
|
||||
uses: pyTooling/Actions/releaser@main
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag: nightly
|
||||
rm: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag_name: nightly
|
||||
prerelease: true
|
||||
files: |
|
||||
build/image/*.zip
|
||||
build/image/build_info.txt
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
45
.idea/misc.xml
generated
45
.idea/misc.xml
generated
@@ -4,7 +4,50 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_25_PREVIEW" project-jdk-name="liberica-25" project-jdk-type="JavaSDK">
|
||||
<component name="NullableNotNullManager">
|
||||
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
|
||||
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
|
||||
<option name="myOrdered" value="true" />
|
||||
<option name="myNullables">
|
||||
<value>
|
||||
<list size="13">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
|
||||
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
|
||||
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||
<item index="8" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
|
||||
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
|
||||
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
|
||||
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
<option name="myNotNulls">
|
||||
<value>
|
||||
<list size="13">
|
||||
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
|
||||
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
|
||||
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||
<item index="8" class="java.lang.String" itemvalue="lombok.NonNull" />
|
||||
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
|
||||
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
|
||||
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
|
||||
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_27_PREVIEW" project-jdk-name="liberica-27" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
120
build.gradle
120
build.gradle
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,16 +17,18 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import org.beryx.runtime.RuntimeZipTask
|
||||
import org.gradle.internal.jvm.Jvm
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Instructions for building/compiling the sdrtrunk application.
|
||||
*
|
||||
* Prerequisites:
|
||||
* Install and configure an OpenJDK version 22+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 8.10+
|
||||
* Install and configure an OpenJDK version 27+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 9.8+
|
||||
*
|
||||
* Scenario 1: run the application via gradle command line from the source code root directory:
|
||||
* command: ./gradlew run
|
||||
@@ -50,24 +52,40 @@ plugins {
|
||||
id 'org.beryx.runtime' version '2.0.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
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 24 is required for this version of the Project Panama preview/incubator feature
|
||||
//Java 27 is required for this version of the Project Panama preview/incubator feature
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
languageVersion = JavaLanguageVersion.of(27)
|
||||
vendor = JvmVendorSpec.BELLSOFT
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs 'src/main'
|
||||
test.java.srcDirs 'src/test'
|
||||
/**
|
||||
* Verifies that the selected toolchain JDK includes the JavaFX modules (e.g. Bellsoft Liberica 'Full JDK').
|
||||
*
|
||||
* Gradle's toolchain spec can only select by language version, vendor and implementation, so it can't distinguish
|
||||
* between the Liberica JDK packages that do and don't bundle JavaFX. This task fails the build with a clear message
|
||||
* instead of allowing compilation to fail with missing javafx packages.
|
||||
*/
|
||||
tasks.register('verifyToolchainJavaFx') {
|
||||
def jdkHome = javaToolchains.launcherFor(java.toolchain).map { it.metadata.installationPath.asFile }
|
||||
|
||||
doLast {
|
||||
def release = new File(jdkHome.get(), 'release')
|
||||
|
||||
if(!release.exists() || !release.text.contains('javafx.base')) {
|
||||
throw new GradleException("The JDK at [${jdkHome.get()}] does not include the JavaFX modules. Install " +
|
||||
"the Bellsoft Liberica JDK 27 'Full' package (includes LibericaFX), or use " +
|
||||
"-Porg.gradle.java.installations.paths=<jdk path> to specify the JDK that Gradle should use.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('compileJava') {
|
||||
dependsOn 'verifyToolchainJavaFx'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -85,37 +103,41 @@ dependencies {
|
||||
|
||||
//Jitpack imports
|
||||
implementation 'com.github.dnault:libresample4j:master'
|
||||
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
|
||||
implementation 'com.github.DSheirer.jmbe:api:v2.1.0'
|
||||
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||
|
||||
//mavenCentral/jcenter imports
|
||||
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 'ch.qos.logback:logback-classic:1.6.3'
|
||||
implementation 'ch.qos.logback:logback-core:1.5.38'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.22.4'
|
||||
implementation 'com.fazecast:jSerialComm:2.11.4'
|
||||
implementation 'com.formdev:flatlaf:3.7.2'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:3.7.2'
|
||||
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.13.2'
|
||||
implementation 'com.google.guava:guava:33.5.0-jre'
|
||||
implementation 'com.github.wendykierp:JTransforms:3.2'
|
||||
implementation 'com.google.code.gson:gson:2.14.0'
|
||||
implementation 'com.google.guava:guava:33.6.0-jre'
|
||||
implementation 'com.jidesoft:jide-oss:3.6.18'
|
||||
implementation 'com.miglayout:miglayout-swing:11.4.2'
|
||||
implementation 'com.miglayout:miglayout-swing:11.4.3'
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'commons-io:commons-io:2.21.0'
|
||||
implementation 'commons-io:commons-io:2.22.0'
|
||||
implementation 'eu.hansolo:charts:1.0.5'
|
||||
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
|
||||
implementation 'io.github.dsheirer:jhydrasdr:1.1.3' //HydraSDR tuners - GitHub release, see settings.gradle
|
||||
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.28.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.19.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.20.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
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.apache.mina:mina-core:2.2.9'
|
||||
implementation 'org.apache.mina:mina-http:2.2.9'
|
||||
implementation 'org.controlsfx:controlsfx:11.2.3'
|
||||
implementation 'org.rauschig:jarchivelib:1.2.0'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||
@@ -123,12 +145,12 @@ dependencies {
|
||||
implementation 'pl.edu.icm:JLargeArrays:1.6'
|
||||
}
|
||||
|
||||
def os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem
|
||||
def os = DefaultNativePlatform.currentOperatingSystem
|
||||
|
||||
/**
|
||||
* This is needed for the JDK23 vector API ... until it moves out of incubation or preview
|
||||
* This is needed for the JDK23+ vector API ... until it moves out of incubation or preview
|
||||
*/
|
||||
tasks.withType(JavaCompile) {
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.compilerArgs.add("--enable-preview") //Project Panama Vector API is still in incubator
|
||||
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Project Panama Vector API is still in incubator
|
||||
}
|
||||
@@ -139,12 +161,11 @@ tasks.withType(JavaCompile) {
|
||||
def jvmArgsWindows =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED', //windows users for JIDE
|
||||
'--add-modules=jdk.incubator.vector', //Project Panama Vector API is still in incubator
|
||||
'--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
|
||||
|
||||
/**
|
||||
@@ -152,10 +173,9 @@ def jvmArgsWindows =
|
||||
*/
|
||||
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
|
||||
'--add-modules=jdk.incubator.vector', //Project Panama vector 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
|
||||
|
||||
@@ -229,7 +249,7 @@ 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/25.0.1+11/bellsoft-jdk25.0.1+11-"
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/27+36/bellsoft-jdk27+36-"
|
||||
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
|
||||
@@ -244,16 +264,17 @@ def jdk_download_windows_aarch64 = jdk_download_base + "windows-aarch64-full.zip
|
||||
* @param jvmArgs to use (windows or linux)
|
||||
* @return configured task.
|
||||
*/
|
||||
def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
def configure(RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
//Use the correct set of JVM arguments
|
||||
rt.extension.launcherData.get().jvmArgs = jvmArgs
|
||||
|
||||
//Additional modules to include with auto-detected modules.
|
||||
//jdk.charsets - NXDN BIG5 encoding support
|
||||
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
|
||||
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
||||
//jdk.accessibility is used with assistive technologies like screen readers
|
||||
//java.management for JVM resource monitoring
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||
rt.extension.addModules('jdk.charsets', 'jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||
|
||||
//Use auto-detected modules and 'add' any specified modules.
|
||||
rt.extension.additive.set(true)
|
||||
@@ -262,7 +283,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
rt.extension.addOptions('--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages')
|
||||
|
||||
//Runtime images have auto appended version in image zip name. Override to include version in single target image.
|
||||
rt.extension.imageZip.set(file("$buildDir/image/sdr-trunk.zip"))
|
||||
// rt.extension.imageDir=layout.buildDirectory.dir('image')
|
||||
rt.extension.imageZip.set(file(layout.buildDirectory.dir('image').get().file('sdr-trunk.zip')))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,35 +296,35 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
* Usage: gradlew runtimeZipCurrent (windows - using gradlew)
|
||||
* Usage: ./gradlew runtimeZipCurrent (linux - using gradlew)
|
||||
*/
|
||||
tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
tasks.register('runtimeZipCurrent', RuntimeZipTask) {rt ->
|
||||
def arch = System.properties.getProperty("os.arch")
|
||||
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
|
||||
def javaHome = Jvm.current().getJavaHome().toString()
|
||||
|
||||
if(os.isWindows()) {
|
||||
if(arch.equals("amd64")) {
|
||||
if(arch == "amd64") {
|
||||
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
|
||||
}
|
||||
else if(arch.equals("aarch64")) {
|
||||
else if(arch == "aarch64") {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64, javaHome)
|
||||
}
|
||||
configure(rt, jvmArgsWindows)
|
||||
}
|
||||
else if(os.isLinux())
|
||||
{
|
||||
if(arch.equals("amd64")) {
|
||||
if(arch == "amd64") {
|
||||
rt.extension.targetPlatform(targetLinuxX86_64, javaHome)
|
||||
}
|
||||
else if(arch.equals("aarch64")) {
|
||||
else if(arch == "aarch64") {
|
||||
rt.extension.targetPlatform(targetLinuxAarch64, javaHome)
|
||||
}
|
||||
configure(rt, jvmArgsLinux)
|
||||
}
|
||||
else if(os.isMacOsX())
|
||||
{
|
||||
if(arch.equals("amd64")) {
|
||||
if(arch == "amd64") {
|
||||
rt.extension.targetPlatform(targetOsxX86_64, javaHome)
|
||||
}
|
||||
else if(arch.equals("aarch64")) {
|
||||
else if(arch == "aarch64") {
|
||||
rt.extension.targetPlatform(targetOsxAarch64, javaHome)
|
||||
}
|
||||
configure(rt, jvmArgsLinux)
|
||||
@@ -319,7 +341,7 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
* Usage: gradlew runtimeZipWindows (windows - using gradlew)
|
||||
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
|
||||
*/
|
||||
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
tasks.register('runtimeZipWindows', RuntimeZipTask) {rt ->
|
||||
//Windows aarch64
|
||||
if(file(jdk_windows_aarch64).exists()) {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, [])
|
||||
@@ -351,7 +373,7 @@ tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
* Usage: gradlew runtimeZipOthers (windows - using gradlew)
|
||||
* Usage: ./gradlew runtimeZipOthers (linux - using gradlew)
|
||||
*/
|
||||
tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
tasks.register('runtimeZipOthers', RuntimeZipTask) {rt ->
|
||||
//Linux aarch64
|
||||
if(file(jdk_linux_aarch64).exists()) {
|
||||
rt.extension.targetPlatform(targetLinuxAarch64, jdk_linux_aarch64, [])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
# Copyright (C) 2014-2026 Dennis Sheirer
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
23
gradle/wrapper/gradle-wrapper.properties
vendored
23
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,26 +1,9 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# 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/>
|
||||
# *****************************************************************************
|
||||
#
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
34
gradlew
vendored
34
gradlew
vendored
@@ -1,27 +1,26 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2025 Dennis Sheirer
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# This 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.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# 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.
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
# *****************************************************************************
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -30,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
@@ -58,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/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/.
|
||||
@@ -115,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -173,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -213,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
|
||||
36
gradlew.bat
vendored
36
gradlew.bat
vendored
@@ -19,12 +19,12 @@
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@@ -51,7 +51,7 @@ 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
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
@@ -65,30 +65,18 @@ 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
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%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" %*
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
@@ -1 +1,28 @@
|
||||
rootProject.name = 'sdr-trunk'
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
||||
}
|
||||
rootProject.name = 'sdr-trunk'
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://jitpack.io' }
|
||||
|
||||
//jHydraSDR is published as a GitHub release, with the native libraries for all platforms in the jar
|
||||
ivy {
|
||||
url = 'https://github.com/DSheirer/jHydraSDR/releases/download'
|
||||
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
|
||||
metadataSources { artifact() }
|
||||
content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
|
||||
}
|
||||
|
||||
//jBladeRF is published as a GitHub release, with the native libraries for all platforms in the jar
|
||||
ivy {
|
||||
url = 'https://github.com/DSheirer/jBladeRF/releases/download'
|
||||
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
|
||||
metadataSources { artifact() }
|
||||
content { includeModule 'io.github.dsheirer', 'jbladerf' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 com.sun.java.swing.plaf.windows;
|
||||
|
||||
import javax.swing.LookAndFeel;
|
||||
import javax.swing.UIDefaults;
|
||||
|
||||
/**
|
||||
* No-op stub for the JDK's Windows look-and-feel.
|
||||
*
|
||||
* <p>JIDE 3.6.18's {@code LookAndFeelFactory} contains compile-time
|
||||
* {@code instanceof com.sun.java.swing.plaf.windows.WindowsLookAndFeel} checks throughout its
|
||||
* dispatch logic. On Linux and macOS JDK builds that class is genuinely absent from
|
||||
* {@code java.desktop}, so the JVM throws {@link NoClassDefFoundError} the first time JIDE's
|
||||
* dispatch code executes the {@code instanceof} bytecode. That happens during
|
||||
* {@link com.jidesoft.swing.JideSplitPane#updateUI()} on every LAF change.
|
||||
*
|
||||
* <p>This stub lives in the classpath so the class reference resolves on non-Windows JDKs.
|
||||
* On Windows JDK builds the real {@code WindowsLookAndFeel} is loaded from the
|
||||
* {@code java.desktop} module via the boot class loader and shadows this class.
|
||||
*
|
||||
* <p>The stub reports itself as not native and not supported - JIDE never instantiates this
|
||||
* class, it only uses it for {@code instanceof} comparisons against the active LAF. Since
|
||||
* sdrtrunk never installs this LAF, the {@code instanceof} check always returns false and
|
||||
* JIDE proceeds to its next branch.
|
||||
*/
|
||||
public class WindowsLookAndFeel extends LookAndFeel
|
||||
{
|
||||
@Override
|
||||
public String getName()
|
||||
{
|
||||
return "Windows (stub)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getID()
|
||||
{
|
||||
return "WindowsStub";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription()
|
||||
{
|
||||
return "Compatibility stub for JIDE's instanceof checks on non-Windows JDKs.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNativeLookAndFeel()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSupportedLookAndFeel()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UIDefaults getDefaults()
|
||||
{
|
||||
return new UIDefaults();
|
||||
}
|
||||
}
|
||||
@@ -30,12 +30,16 @@ import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
||||
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
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.NXDNTypeDTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
@@ -79,6 +83,31 @@ public class AliasFactory
|
||||
Min copyMin = new Min();
|
||||
copyMin.setMin(originalMin.getMin());
|
||||
return copyMin;
|
||||
case NXDN_TYPE_D_RADIO_ID:
|
||||
NXDNTypeDRadio originalTypeDRadio = (NXDNTypeDRadio)id;
|
||||
NXDNTypeDRadio copyTypeDRadio = new NXDNTypeDRadio(originalTypeDRadio.getHomeRepeater(),
|
||||
originalTypeDRadio.getIdentifier());
|
||||
copyTypeDRadio.setOverlap(originalTypeDRadio.overlapProperty().get());
|
||||
return copyTypeDRadio;
|
||||
case NXDN_TYPE_D_RADIO_ID_RANGE:
|
||||
NXDNTypeDRadioRange originalTypeDRadioRange = (NXDNTypeDRadioRange)id;
|
||||
NXDNTypeDRadioRange copyTypeDRadioRange = new NXDNTypeDRadioRange(originalTypeDRadioRange.getHomeRepeater(),
|
||||
originalTypeDRadioRange.getMinIdentifier(), originalTypeDRadioRange.getMaxIdentifier());
|
||||
copyTypeDRadioRange.setOverlap(originalTypeDRadioRange.overlapProperty().get());
|
||||
return copyTypeDRadioRange;
|
||||
case NXDN_TYPE_D_TALKGROUP:
|
||||
NXDNTypeDTalkgroup originalTypeDTalkgroup = (NXDNTypeDTalkgroup)id;
|
||||
NXDNTypeDTalkgroup copyTypeDTalkgroup = new NXDNTypeDTalkgroup(originalTypeDTalkgroup.getHomeRepeater(),
|
||||
originalTypeDTalkgroup.getIdentifier());
|
||||
copyTypeDTalkgroup.setOverlap(originalTypeDTalkgroup.overlapProperty().get());
|
||||
return copyTypeDTalkgroup;
|
||||
case NXDN_TYPE_D_TALKGROUP_RANGE:
|
||||
NXDNTypeDTalkgroupRange originalTypeDTalkgroupRange = (NXDNTypeDTalkgroupRange)id;
|
||||
NXDNTypeDTalkgroupRange copyTypeDTalkgroupRange = new NXDNTypeDTalkgroupRange(
|
||||
originalTypeDTalkgroupRange.getHomeRepeater(), originalTypeDTalkgroupRange.getMinIdentifier(),
|
||||
originalTypeDTalkgroupRange.getMaxIdentifier());
|
||||
copyTypeDTalkgroupRange.setOverlap(originalTypeDTalkgroupRange.overlapProperty().get());
|
||||
return copyTypeDTalkgroupRange;
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
|
||||
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),
|
||||
|
||||
@@ -47,7 +47,7 @@ import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -134,6 +134,7 @@ public class AliasList
|
||||
switch(id.getType())
|
||||
{
|
||||
case TALKGROUP:
|
||||
case NXDN_TYPE_D_TALKGROUP:
|
||||
Talkgroup talkgroup = (Talkgroup)id;
|
||||
|
||||
TalkgroupAliasList talkgroupAliasList = mTalkgroupProtocolMap.get(talkgroup.getProtocol());
|
||||
@@ -147,6 +148,7 @@ public class AliasList
|
||||
talkgroupAliasList.add(talkgroup, alias);
|
||||
break;
|
||||
case TALKGROUP_RANGE:
|
||||
case NXDN_TYPE_D_TALKGROUP_RANGE:
|
||||
TalkgroupRange talkgroupRange = (TalkgroupRange)id;
|
||||
|
||||
TalkgroupAliasList talkgroupRangeAliasList = mTalkgroupProtocolMap.get(talkgroupRange.getProtocol());
|
||||
@@ -186,6 +188,7 @@ public class AliasList
|
||||
p25TalkgroupAliasList.add(qualifiedTalkgroup, alias);
|
||||
break;
|
||||
case RADIO_ID:
|
||||
case NXDN_TYPE_D_RADIO_ID:
|
||||
Radio radio = (Radio)id;
|
||||
|
||||
RadioAliasList radioAliasList = mRadioProtocolMap.get(radio.getProtocol());
|
||||
@@ -199,6 +202,7 @@ public class AliasList
|
||||
radioAliasList.add(radio, alias);
|
||||
break;
|
||||
case RADIO_ID_RANGE:
|
||||
case NXDN_TYPE_D_RADIO_ID_RANGE:
|
||||
RadioRange radioRange = (RadioRange)id;
|
||||
|
||||
RadioAliasList radioRangeAliasList = mRadioProtocolMap.get(radioRange.getProtocol());
|
||||
|
||||
@@ -35,12 +35,16 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio;
|
||||
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
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.NXDNTypeDTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
@@ -64,6 +68,10 @@ import javafx.util.Callback;
|
||||
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
||||
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
||||
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDRadio.class, name = "nxdnTypeDRadio"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDRadioRange.class, name = "nxdnTypeDRadioRange"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDTalkgroup.class, name = "nxdnTypeDTalkgroup"),
|
||||
@JsonSubTypes.Type(value = NXDNTypeDTalkgroupRange.class, name = "nxdnTypeDTalkgroupRange"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
|
||||
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
|
||||
|
||||
@@ -29,6 +29,10 @@ public enum AliasIDType
|
||||
LOJACK("LoJack"),
|
||||
LTR_NET_UID("LTR-Net UID"),
|
||||
MIN("Passport MIN"),
|
||||
NXDN_TYPE_D_RADIO_ID("NXDN Type-D Radio ID"),
|
||||
NXDN_TYPE_D_RADIO_ID_RANGE("NXDN Type-D Radio ID Range"),
|
||||
NXDN_TYPE_D_TALKGROUP("NXDN Type-D Talkgroup"),
|
||||
NXDN_TYPE_D_TALKGROUP_RANGE("NXDN Type-D Talkgroup Range"),
|
||||
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
|
||||
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
|
||||
PRIORITY("Audio Priority"),
|
||||
@@ -58,6 +62,7 @@ public enum AliasIDType
|
||||
|
||||
//Values used by the View-By alias editor
|
||||
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
|
||||
NXDN_TYPE_D_RADIO_ID, NXDN_TYPE_D_RADIO_ID_RANGE, NXDN_TYPE_D_TALKGROUP, NXDN_TYPE_D_TALKGROUP_RANGE,
|
||||
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
|
||||
|
||||
public String toString()
|
||||
|
||||
46
src/main/java/io/github/dsheirer/alias/id/INXDNTypeD.java
Normal file
46
src/main/java/io/github/dsheirer/alias/id/INXDNTypeD.java
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
/**
|
||||
* NXDN Type-D alias identifier composed of a home repeater and a radio or talkgroup identifier value.
|
||||
*/
|
||||
public interface INXDNTypeD
|
||||
{
|
||||
/**
|
||||
* Home repeater for the identifier
|
||||
*/
|
||||
int getHomeRepeater();
|
||||
|
||||
/**
|
||||
* Sets the home repeater for the identifier
|
||||
*/
|
||||
void setHomeRepeater(int homeRepeater);
|
||||
|
||||
/**
|
||||
* Radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
int getIdentifier();
|
||||
|
||||
/**
|
||||
* Sets the radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
void setIdentifier(int identifier);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
/**
|
||||
* NXDN Type-D alias identifier range of radio or talkgroup values that share a common home repeater.
|
||||
*/
|
||||
public interface INXDNTypeDRange
|
||||
{
|
||||
/**
|
||||
* Home repeater for the range
|
||||
*/
|
||||
int getHomeRepeater();
|
||||
|
||||
/**
|
||||
* Sets the home repeater for the range
|
||||
*/
|
||||
void setHomeRepeater(int homeRepeater);
|
||||
|
||||
/**
|
||||
* Minimum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
int getMinIdentifier();
|
||||
|
||||
/**
|
||||
* Sets the minimum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
void setMinIdentifier(int identifier);
|
||||
|
||||
/**
|
||||
* Maximum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
int getMaxIdentifier();
|
||||
|
||||
/**
|
||||
* Sets the maximum radio or talkgroup value, without the home repeater.
|
||||
*/
|
||||
void setMaxIdentifier(int identifier);
|
||||
}
|
||||
@@ -22,7 +22,7 @@ package io.github.dsheirer.alias.id.dcs;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
|
||||
|
||||
/**
|
||||
* Digital Coded Squelch (DCS) tone identifier
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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.radio;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeD;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D radio. The value is the over-the-air 16-bit identifier that combines the home repeater and radio so that
|
||||
* it matches the value of an NXDN Type-D radio identifier.
|
||||
*/
|
||||
public class NXDNTypeDRadio extends Radio implements INXDNTypeD
|
||||
{
|
||||
public NXDNTypeDRadio()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the radio
|
||||
* @param radio value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDRadio(int homeRepeater, int radio)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, radio));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_RADIO_ID;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier()));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdentifier(int identifier)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Radio ID:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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.radio;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeDRange;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D radio range for a single home repeater. The min and max values are the over-the-air 16-bit identifiers
|
||||
* that combine the home repeater and radio.
|
||||
*/
|
||||
public class NXDNTypeDRadioRange extends RadioRange implements INXDNTypeDRange
|
||||
{
|
||||
public NXDNTypeDRadioRange()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the range
|
||||
* @param minRadio value, without the home repeater
|
||||
* @param maxRadio value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDRadioRange(int homeRepeater, int minRadio, int maxRadio)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minRadio),
|
||||
NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxRadio));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_RADIO_ID_RANGE;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
int min = getMinIdentifier();
|
||||
int max = getMaxIdentifier();
|
||||
setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min));
|
||||
setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMinIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinIdentifier(int identifier)
|
||||
{
|
||||
setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMaxIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdentifier(int identifier)
|
||||
{
|
||||
setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid when the base range is valid and both ends of the range use the same home repeater.
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return super.isValid() && getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxRadio());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Radio ID Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true));
|
||||
sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.id.radio;
|
||||
@@ -38,10 +35,11 @@ public enum RadioFormat
|
||||
"<html>APCO25 valid range is 0 to 16,777,215"),
|
||||
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
|
||||
"<html>DMR unit id valid range is 0 to 16,777,215"),
|
||||
NXDN("*****", 1, 0xFFFF, "1 to 65,535", "NXDN valid range is 1 to 65,535"),
|
||||
PASSPORT("********", 0, 0x7FFFFF, "0 to 8,388,607",
|
||||
"<html>PASSPORT valid range is 0 to 8,388,607"),
|
||||
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||
"Unknown protocol valid value range is 1-16,777,215");
|
||||
"<html>Unknown protocol valid value range is 1-16,777,215");
|
||||
|
||||
private String mMask;
|
||||
private int mMinimumValue;
|
||||
@@ -116,6 +114,8 @@ public enum RadioFormat
|
||||
return APCO25;
|
||||
case DMR:
|
||||
return DMR;
|
||||
case NXDN:
|
||||
return NXDN;
|
||||
case PASSPORT:
|
||||
return PASSPORT;
|
||||
default:
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.radio;
|
||||
@@ -26,14 +23,14 @@ import io.github.dsheirer.preference.identifier.IntegerFormat;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Protocol-specific formatting of radio id values and parsing of formatted radio id values.
|
||||
@@ -47,6 +44,7 @@ public class RadioFormatter
|
||||
{
|
||||
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeD;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D talkgroup. The value is the over-the-air 16-bit identifier that combines the home repeater and talkgroup
|
||||
* so that it matches the value of an NXDN Type-D talkgroup identifier.
|
||||
*/
|
||||
public class NXDNTypeDTalkgroup extends Talkgroup implements INXDNTypeD
|
||||
{
|
||||
public NXDNTypeDTalkgroup()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the talkgroup
|
||||
* @param talkgroup value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDTalkgroup(int homeRepeater, int talkgroup)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, talkgroup));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_TALKGROUP;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier()));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIdentifier(int identifier)
|
||||
{
|
||||
setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Talkgroup:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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 com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.INXDNTypeDRange;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* NXDN Type-D talkgroup range for a single home repeater. The min and max values are the over-the-air 16-bit
|
||||
* identifiers that combine the home repeater and talkgroup.
|
||||
*/
|
||||
public class NXDNTypeDTalkgroupRange extends TalkgroupRange implements INXDNTypeDRange
|
||||
{
|
||||
public NXDNTypeDTalkgroupRange()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
setProtocol(Protocol.NXDN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param homeRepeater for the range
|
||||
* @param minTalkgroup value, without the home repeater
|
||||
* @param maxTalkgroup value, without the home repeater
|
||||
*/
|
||||
public NXDNTypeDTalkgroupRange(int homeRepeater, int minTalkgroup, int maxTalkgroup)
|
||||
{
|
||||
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minTalkgroup),
|
||||
NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxTalkgroup));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.NXDN_TYPE_D_TALKGROUP_RANGE;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getHomeRepeater()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHomeRepeater(int homeRepeater)
|
||||
{
|
||||
int min = getMinIdentifier();
|
||||
int max = getMaxIdentifier();
|
||||
setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min));
|
||||
setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMinIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMinIdentifier(int identifier)
|
||||
{
|
||||
setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getMaxIdentifier()
|
||||
{
|
||||
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxIdentifier(int identifier)
|
||||
{
|
||||
setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid when the base range is valid and both ends of the range use the same home repeater.
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return super.isValid() &&
|
||||
getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxTalkgroup());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Type-D Talkgroup Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true));
|
||||
sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true));
|
||||
sb.append(" Protocol:").append(getProtocol());
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 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.id.talkgroup;
|
||||
@@ -50,6 +47,8 @@ public enum TalkgroupFormat
|
||||
"<html>MPT-1327 valid ranges are 0-127(prefix)<br>and 1-8192(ident) (ie. 000-0001 to 127-8192)"),
|
||||
NBFM("*****", 1, 0xFFFF, "1 to 65,535",
|
||||
"NBFM valid value range is 1-65,535"),
|
||||
NXDN("*****", 0, 0xFFFF, "0 to 65,535",
|
||||
"NXDN talkgroup valid range is 0 to 65,535"),
|
||||
PASSPORT("*****", 1, 0xFFFF, "1 to 65,535",
|
||||
"Passport valid value range is 1-65,535"),
|
||||
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||
@@ -139,6 +138,8 @@ public enum TalkgroupFormat
|
||||
return MPT1327;
|
||||
case NBFM:
|
||||
return NBFM;
|
||||
case NXDN:
|
||||
return NXDN;
|
||||
case PASSPORT:
|
||||
return PASSPORT;
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -28,6 +28,7 @@ import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupForm
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter;
|
||||
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
@@ -59,6 +60,7 @@ public class TalkgroupFormatter
|
||||
mFormatterMap.put(Protocol.LTR_NET, ltr);
|
||||
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
|
||||
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
/**
|
||||
* Used for defining an array list of filter methods to be implemented in AudioModule
|
||||
* The filters will be applied in the order given in the list.
|
||||
* Each audio filter mush implement the filter method.
|
||||
*/
|
||||
|
||||
public abstract class AbstractAudioFilter
|
||||
{
|
||||
/**
|
||||
* To be overridden by each audio filter
|
||||
* @param audio a buffer of 8 KHz audio samples
|
||||
* @return float[] filtered buffer of 8 KHz audio samples
|
||||
*/
|
||||
public abstract float[] filter(float[] audio);
|
||||
}
|
||||
44
src/main/java/io/github/dsheirer/audio/AudioGainFilter.java
Normal file
44
src/main/java/io/github/dsheirer/audio/AudioGainFilter.java
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
import io.github.dsheirer.dsp.gain.AudioGainAndDcFilter;
|
||||
|
||||
/**
|
||||
* This is a wrapper for the AudioGainAndDcFilter class so it can be used with the AudioModule filter list.
|
||||
* Usually instantiated in the DecoderFactory
|
||||
*/
|
||||
public class AudioGainFilter extends AbstractAudioFilter
|
||||
{
|
||||
AudioGainAndDcFilter mAudioGain;
|
||||
|
||||
public AudioGainFilter(float minGain, float maxGain, float objectiveAmplitude)
|
||||
{
|
||||
mAudioGain = new AudioGainAndDcFilter(minGain, maxGain, objectiveAmplitude);
|
||||
mAudioGain.setDecayRate(2); // to remove pumping effect
|
||||
mAudioGain.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] filter(float[] audio)
|
||||
{
|
||||
return mAudioGain.process(audio);
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ import io.github.dsheirer.sample.real.IRealBufferListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides packaging of demodulated audio sample buffers into audio segments for broadcast to registered listeners.
|
||||
* Includes audio packet metadata in constructed audio segments.
|
||||
@@ -42,36 +44,7 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
Listener<float[]>
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
private final boolean mAudioFilterEnable;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopBandCutoff(200)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.025)
|
||||
.passBandStart(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.build();
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
sHighPassFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
private List<AbstractAudioFilter>mAudioFilters;
|
||||
private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||
|
||||
@@ -81,23 +54,23 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param timeslot for this audio module
|
||||
* @param maxAudioSegmentLength in milliseconds
|
||||
* @param audioFilterEnable to enable or disable high-pass audio filter
|
||||
* @param audioFilters a list of audio filters to apply to the incoming audio samples
|
||||
*/
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, boolean audioFilterEnable)
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, List<AbstractAudioFilter> audioFilters)
|
||||
{
|
||||
super(aliasList, timeslot, maxAudioSegmentLength);
|
||||
mAudioFilterEnable = audioFilterEnable;
|
||||
mAudioFilters = audioFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param audioFilterEnable to enable or disable high-pass audio filter
|
||||
* @param audioFilters a list of audio filters to apply to the incoming audio samples
|
||||
*/
|
||||
public AudioModule(AliasList aliasList, boolean audioFilterEnable)
|
||||
public AudioModule(AliasList aliasList, List<AbstractAudioFilter> audioFilters)
|
||||
{
|
||||
super(aliasList);
|
||||
mAudioFilterEnable = audioFilterEnable;
|
||||
mAudioFilters = audioFilters;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,9 +101,13 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
{
|
||||
if(mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
if(mAudioFilterEnable)
|
||||
// apply audio filters if any (i.e. deemphasis, high pass, etc.)
|
||||
if(!mAudioFilters.isEmpty())
|
||||
{
|
||||
audioBuffer = mHighPassFilter.filter(audioBuffer);
|
||||
for(AbstractAudioFilter filter : mAudioFilters)
|
||||
{
|
||||
audioBuffer = filter.filter(audioBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
addAudio(audioBuffer);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -61,7 +61,7 @@ import org.slf4j.LoggerFactory;
|
||||
*
|
||||
* Producers will add audio buffers and update identifiers throughout the life-cycle of an audio segment. The producer
|
||||
* will signal the completion of an audio segment by setting the complete property to true. This allows consumers the
|
||||
* option to process the audio buffers throughout the life-cycle of the segment, or to process all of the buffers once
|
||||
* option to process the audio buffers throughout the life-cycle of the segment, or to process all the buffers once
|
||||
* the segment is complete.
|
||||
*/
|
||||
public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class HighPassAudioFilter extends AbstractAudioFilter
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(HighPassAudioFilter.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.stopBandCutoff(200)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.025)
|
||||
.passBandStart(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.build();
|
||||
try
|
||||
{
|
||||
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
|
||||
|
||||
if(designer.isValid())
|
||||
{
|
||||
sHighPassFilterCoefficients = designer.getImpulseResponse();
|
||||
}
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Filter design error", fde);
|
||||
}
|
||||
}
|
||||
|
||||
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
|
||||
@Override
|
||||
public float[] filter(float[] audio)
|
||||
{
|
||||
audio = mHighPassFilter.filter(audio);
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,27 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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;
|
||||
|
||||
import io.github.dsheirer.audio.playback.AudioOutput;
|
||||
import io.github.dsheirer.audio.playback.AudioChannel;
|
||||
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceDescriptor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -12,13 +30,13 @@ import java.util.List;
|
||||
public interface IAudioController
|
||||
{
|
||||
/* Current Mixer and MixerChannel configuration */
|
||||
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException;
|
||||
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException;
|
||||
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor device ) throws AudioException;
|
||||
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice() throws AudioException;
|
||||
|
||||
/* Audio Output(s) */
|
||||
public List<AudioOutput> getAudioOutputs();
|
||||
/* Audio Channel(s) */
|
||||
public List<AudioChannel> getAudioChannels();
|
||||
|
||||
/* Controller Audio Event Listener */
|
||||
public void addControllerListener( Listener<AudioEvent> listener );
|
||||
public void removeControllerListener( Listener<AudioEvent> listener );
|
||||
public void addAudioEventListener(Listener<AudioEvent> listener );
|
||||
public void removeAudioEventListener(Listener<AudioEvent> listener );
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY);
|
||||
protected ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
|
||||
protected int mStreamedAudioCount = 0;
|
||||
protected int mDuplicateRejectedAudioCount = 0;
|
||||
protected int mErrorAudioCount = 0;
|
||||
protected int mAgedOffAudioCount = 0;
|
||||
|
||||
@@ -163,6 +164,22 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
mStreamedAudioCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of audio recordings rejected by the remote service as duplicates.
|
||||
*/
|
||||
public int getDuplicateRejectedAudioCount()
|
||||
{
|
||||
return mDuplicateRejectedAudioCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the duplicate-rejected audio count by one.
|
||||
*/
|
||||
public void incrementDuplicateRejectedAudioCount()
|
||||
{
|
||||
mDuplicateRejectedAudioCount++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of audio recordings that were removed for exceeding age limit
|
||||
*/
|
||||
|
||||
@@ -82,6 +82,7 @@ public class BroadcastEvent
|
||||
BROADCASTER_QUEUE_CHANGE,
|
||||
BROADCASTER_STATE_CHANGE,
|
||||
BROADCASTER_STREAMED_COUNT_CHANGE,
|
||||
BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE,
|
||||
BROADCASTER_AGED_OFF_COUNT_CHANGE,
|
||||
BROADCASTER_ERROR_COUNT_CHANGE,
|
||||
BROADCASTER_DELETE,
|
||||
|
||||
@@ -62,11 +62,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
public static final int COLUMN_BROADCASTER_STATUS = 2;
|
||||
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
|
||||
public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
|
||||
public static final int COLUMN_BROADCASTER_AGED_OFF_COUNT = 5;
|
||||
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 6;
|
||||
public static final int COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT = 5;
|
||||
public static final int COLUMN_BROADCASTER_AGED_OFF_COUNT = 6;
|
||||
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 7;
|
||||
|
||||
public static final String[] COLUMN_NAMES = new String[]
|
||||
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
|
||||
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Duplicate Rejected", "Aged Off", "Upload Error"};
|
||||
|
||||
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
|
||||
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
|
||||
@@ -361,7 +362,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
|
||||
ThreadPool.CACHED.submit(audioBroadcaster::start);
|
||||
ThreadPool.CACHED.submit(() -> audioBroadcaster.start());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,6 +532,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
|
||||
}
|
||||
break;
|
||||
case BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
fireTableCellUpdated(row, COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT);
|
||||
}
|
||||
break;
|
||||
case BROADCASTER_AGED_OFF_COUNT_CHANGE:
|
||||
if(row >= 0)
|
||||
{
|
||||
@@ -607,6 +614,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
return configuredBroadcast.getAudioBroadcaster().getStreamedAudioCount();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT:
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
return configuredBroadcast.getAudioBroadcaster().getDuplicateRejectedAudioCount();
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
|
||||
if(configuredBroadcast.hasAudioBroadcaster())
|
||||
{
|
||||
@@ -643,6 +656,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
|
||||
case COLUMN_BROADCASTER_QUEUE_SIZE:
|
||||
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||
case COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT:
|
||||
case COLUMN_BROADCASTER_ERROR_COUNT:
|
||||
return Integer.class;
|
||||
case COLUMN_BROADCAST_SERVER_TYPE:
|
||||
|
||||
@@ -151,7 +151,10 @@ public class BroadcastStatusPanel extends JPanel
|
||||
else if(state == BroadcastState.DISABLED)
|
||||
{
|
||||
setBackground(table.getBackground());
|
||||
setForeground(Color.LIGHT_GRAY);
|
||||
//Use the LAF's disabled-foreground so the cell reads correctly on any
|
||||
//theme - LIGHT_GRAY is invisible on a light background.
|
||||
Color disabled = javax.swing.UIManager.getColor("Label.disabledForeground");
|
||||
setForeground(disabled != null ? disabled : table.getForeground());
|
||||
}
|
||||
else if(state == BroadcastState.INVALID_SETTINGS ||
|
||||
state == BroadcastState.NETWORK_UNAVAILABLE)
|
||||
|
||||
@@ -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.AliasListConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
@@ -253,6 +254,8 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
String radioId = getFrom(audioRecording);
|
||||
float frequency = getFrequency(audioRecording);
|
||||
|
||||
String radioIdAlias = getFromAlias(audioRecording);
|
||||
|
||||
BroadcastifyCallBuilder bodyBuilder = new BroadcastifyCallBuilder();
|
||||
bodyBuilder.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||
.addPart(FormField.SYSTEM_ID, getBroadcastConfiguration().getSystemID())
|
||||
@@ -263,6 +266,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
.addPart(FormField.FREQUENCY, frequency)
|
||||
.addPart(FormField.ENCODING, ENCODING_TYPE_MP3);
|
||||
|
||||
if(radioIdAlias != null && !radioIdAlias.isEmpty())
|
||||
{
|
||||
bodyBuilder.addPart(FormField.RADIO_ID_ALIAS, radioIdAlias);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
@@ -362,6 +370,9 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
else if(urlResponse.startsWith("1 SKIPPED"))
|
||||
{
|
||||
//Broadcastify is telling us to skip audio upload - someone already uploaded it
|
||||
incrementDuplicateRejectedAudioCount();
|
||||
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
else
|
||||
@@ -446,6 +457,27 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the over-the-air talker alias for the FROM (source radio) identifier, acquired in real-time from RF
|
||||
* (e.g. P25 Motorola Talker Alias or DMR Talker Alias). Returns null if no OTA alias was received for this call.
|
||||
* This is sent as the srcId_alias field in Broadcastify uploads.
|
||||
*/
|
||||
private static String getFromAlias(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof TalkerAliasIdentifier talkerAliasIdentifier)
|
||||
{
|
||||
if(talkerAliasIdentifier.isValid())
|
||||
{
|
||||
return talkerAliasIdentifier.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*/
|
||||
|
||||
@@ -30,6 +30,7 @@ public enum FormField
|
||||
TIMESTAMP("ts"),
|
||||
TALKGROUP_ID("tg"),
|
||||
RADIO_ID("src"),
|
||||
RADIO_ID_ALIAS("srcId_alias"),
|
||||
FREQUENCY("freq"),
|
||||
ENCODING("enc"),
|
||||
TEST("test");
|
||||
|
||||
@@ -55,6 +55,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
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";
|
||||
private static final String HTTP_1_1_CONTINUE_HEX_DUMP = "48 54 54 50 2F 31 2E 31 20 31 30 30";
|
||||
|
||||
private NioSocketConnector mSocketConnector;
|
||||
private IoSession mStreamingSession = null;
|
||||
@@ -262,6 +263,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
mHTTPHeaders = new HashMap<>();
|
||||
mHTTPHeaders.put(IcecastHeader.ACCEPT.getValue(), "*/*");
|
||||
mHTTPHeaders.put(IcecastHeader.EXPECT.getValue(), "100-continue");
|
||||
mHTTPHeaders.put(IcecastHeader.CONTENT_TYPE.getValue(), getConfiguration().getBroadcastFormat().getValue());
|
||||
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
|
||||
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getBase64EncodedCredentials());
|
||||
@@ -350,7 +352,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
String hexDump = ((ProtocolDecoderException)throwable).getHexdump();
|
||||
|
||||
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
|
||||
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP) || hexDump.startsWith(HTTP_1_1_CONTINUE_HEX_DUMP))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnectionFailureLogged.set(false);
|
||||
@@ -432,6 +434,10 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
switch(response.getStatus())
|
||||
{
|
||||
case INFORMATIONAL_CONTINUE:
|
||||
//Icecast 2.5+ signals that we can start sending audio with 100-continue
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
mConnecting.set(false);
|
||||
mConnectionFailureLogged.set(false);
|
||||
break;
|
||||
case SUCCESS_OK:
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
@@ -500,7 +506,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
|
||||
mMessage = new String(bytes);
|
||||
|
||||
Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
|
||||
//Icecast 2.4+ responds with HTTP/1.0 and Icecast 2.5+ responds with HTTP/1.1
|
||||
Pattern pattern = Pattern.compile("HTTP/1\\.[01] (\\d{3})");
|
||||
Matcher m = pattern.matcher(mMessage);
|
||||
|
||||
if(m.find())
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.gui.playlist.streaming.AbstractBroadcastEditor;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
|
||||
@@ -35,9 +35,12 @@ import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import jmbe.iface.IAudioCodec;
|
||||
import jmbe.iface.IAudioCodecLibrary;
|
||||
import jmbe.iface.IAudioCodecLibraryV2;
|
||||
import jmbe.iface.IAudioCodecV2;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -47,7 +50,7 @@ import org.slf4j.LoggerFactory;
|
||||
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
|
||||
ISquelchStateListener
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JmbeAudioModule.class);
|
||||
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
|
||||
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
|
||||
private IAudioCodec mAudioCodec;
|
||||
@@ -62,6 +65,37 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
loadConverter();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of features supported by the JMBE audio library. Note: features are only supported in Version 2.0+
|
||||
* @return list of features or an empty list if the current library version doesn't support features.
|
||||
*/
|
||||
public List<String> getFeatures()
|
||||
{
|
||||
if(mAudioCodec instanceof IAudioCodecV2 v2)
|
||||
{
|
||||
return v2.getFeatures();
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JMBE feature
|
||||
* @param name of the feature
|
||||
* @param value for the feature
|
||||
*/
|
||||
public void setFeature(String name, Object value)
|
||||
{
|
||||
if(mAudioCodec instanceof IAudioCodecV2 v2)
|
||||
{
|
||||
v2.setFeature(name, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG.info("Feature [" + name + "] is not supported by the current JMBE audio codec library version");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeAudioSegment()
|
||||
{
|
||||
@@ -135,7 +169,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.info("Loading JMBE library from [" + path + "]");
|
||||
LOG.info("Loading JMBE library from [" + path + "]");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -143,13 +177,32 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
|
||||
this.getClass().getClassLoader());
|
||||
|
||||
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
|
||||
try
|
||||
{
|
||||
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibraryV2", true, childClassLoader);
|
||||
}
|
||||
catch(ClassNotFoundException legacyLibrary)
|
||||
{
|
||||
try
|
||||
{
|
||||
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
|
||||
}
|
||||
catch(ClassNotFoundException missingLibrary)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
LOG.error("Couldn't load JMBE audio conversion library from [" + path +
|
||||
"] - neither V2 nor V1 library class was found");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
|
||||
LOG.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
|
||||
}
|
||||
}
|
||||
@@ -157,18 +210,11 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
catch(ClassNotFoundException e1)
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - class not found");
|
||||
LOG.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,11 +229,23 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
if((library.getMajorVersion() == 1 && library.getMinorVersion() >= 0 &&
|
||||
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
|
||||
{
|
||||
audioConverter = library.getAudioConverter(getCodecName());
|
||||
audioConverter = library instanceof IAudioCodecLibraryV2 libraryV2 ?
|
||||
libraryV2.getAudioConverterV2(getCodecName()) :
|
||||
library.getAudioConverter(getCodecName());
|
||||
|
||||
String codecStatusKey = JMBE_AUDIO_LIBRARY + ":codec:" + getCodecName();
|
||||
if(!mLibraryLoadStatusLogged.contains(codecStatusKey))
|
||||
{
|
||||
LOG.info("JMBE codec [" + getCodecName() + "] class [" +
|
||||
audioConverter.getClass().getName() + "] features [" +
|
||||
(audioConverter instanceof IAudioCodecV2 v2 ?
|
||||
v2.getFeatures() : Collections.emptyList()) + "]");
|
||||
mLibraryLoadStatusLogged.add(codecStatusKey);
|
||||
}
|
||||
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.info("JMBE audio conversion library loaded: " + library.getVersion());
|
||||
LOG.info("JMBE audio conversion library loaded: " + library.getVersion());
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -195,7 +253,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.warn("JMBE library version 1.0.0 or higher is required - found: " + library.getVersion());
|
||||
LOG.warn("JMBE library version 1.0.0 or higher is required - found: " + library.getVersion());
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -204,7 +262,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.info("JMBE audio conversion library NOT FOUND");
|
||||
LOG.info("JMBE audio conversion library NOT FOUND");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -213,7 +271,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - invocation target exception", ite);
|
||||
LOG.error("Couldn't load JMBE audio conversion library - invocation target exception", ite);
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -221,7 +279,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - instantiation exception", e1);
|
||||
LOG.error("Couldn't load JMBE audio conversion library - instantiation exception", e1);
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -229,7 +287,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - security restrictions");
|
||||
LOG.error("Couldn't load JMBE audio conversion library - security restrictions");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -237,7 +295,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
|
||||
LOG.error("Couldn't load JMBE audio conversion library - no such method exception");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
@@ -246,7 +304,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
{
|
||||
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
|
||||
{
|
||||
mLog.warn("JMBE audio library path is NOT SET in your User Preferences.");
|
||||
LOG.warn("JMBE audio library path is NOT SET in your User Preferences.");
|
||||
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
import com.fasterxml.jackson.annotation.JsonRootName;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKey;
|
||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -288,10 +287,18 @@ public class MBECallSequence
|
||||
}
|
||||
else
|
||||
{
|
||||
mVoiceFrames.add(new VoiceFrame(timestamp, frame));
|
||||
add(new VoiceFrame(timestamp, frame));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the voice frame to the sequence
|
||||
*/
|
||||
public void add(VoiceFrame voiceFrame)
|
||||
{
|
||||
mVoiceFrames.add(voiceFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an encrypted audio frame to this call sequence
|
||||
*
|
||||
@@ -317,6 +324,6 @@ public class MBECallSequence
|
||||
*/
|
||||
public void addEncryptedVoiceFrame(long timestamp, String frame, int algorithm, int keyid, String messageIndicator)
|
||||
{
|
||||
mVoiceFrames.add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
|
||||
add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -25,7 +25,6 @@ import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
|
||||
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
|
||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
|
||||
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
@@ -25,7 +22,6 @@ package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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.module.decode.p25.audio;
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
@@ -27,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonRootName;
|
||||
*/
|
||||
@JsonRootName(value = "frame")
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
@JsonPropertyOrder({"encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
|
||||
@JsonPropertyOrder({"tag", "encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
|
||||
public class VoiceFrame
|
||||
{
|
||||
private long mTimestamp;
|
||||
@@ -35,6 +38,7 @@ public class VoiceFrame
|
||||
private Integer mAlgorithm;
|
||||
private Integer mKeyId;
|
||||
private String mMessageIndicator;
|
||||
private String mTag;
|
||||
|
||||
public VoiceFrame()
|
||||
{
|
||||
@@ -70,6 +74,24 @@ public class VoiceFrame
|
||||
mMessageIndicator = messageIndicator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag value for the voice frame
|
||||
*/
|
||||
@JsonProperty("tag")
|
||||
public String getTag()
|
||||
{
|
||||
return mTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the optional tag value for the frame
|
||||
* @param tag to set
|
||||
*/
|
||||
public void setTag(String tag)
|
||||
{
|
||||
mTag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp the frame was transmitted
|
||||
*
|
||||
@@ -0,0 +1,638 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Manages the incoming audio segments and provides access to audio buffers from those segments.
|
||||
*/
|
||||
public class AudioChannel implements Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
/**
|
||||
* Quantity of audio samples to deliver per getAudio() method invocation, equivalent to 20 milliseconds at an 8 kHz
|
||||
* sample rate.
|
||||
*/
|
||||
public static final int SAMPLES_PER_INTERVAL = 160;
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AudioChannel.class);
|
||||
private final AudioBuffer mAudioBuffer = new AudioBuffer();
|
||||
private final Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||
private final LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private final UserPreferences mUserPreferences;
|
||||
private final String mChannelName;
|
||||
|
||||
private AudioSegment mCurrentAudioSegment;
|
||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||
private boolean mDropDuplicates;
|
||||
private boolean mMetadataSent = false;
|
||||
private boolean mMuted;
|
||||
private boolean mDisabled;
|
||||
private float[] mAudioSegmentStartTone;
|
||||
private float[] mAudioSegmentDropTone;
|
||||
private int mCurrentBufferIndex = -1;
|
||||
private int mNoAudioFromSegmentIntervalCount = 0;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public AudioChannel(UserPreferences userPreferences, String channelName)
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
mChannelName = channelName;
|
||||
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
updateToneInsertionAudioClips();
|
||||
|
||||
//Register to receive preference update notifications so we can update the preference items
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables this audio channel.
|
||||
* @param disabled state
|
||||
*/
|
||||
public void setDisabled(boolean disabled)
|
||||
{
|
||||
mDisabled = disabled;
|
||||
|
||||
if(disabled)
|
||||
{
|
||||
AudioSegment segment = mAudioSegmentQueue.poll();
|
||||
|
||||
while(segment != null)
|
||||
{
|
||||
segment.decrementConsumerCount();
|
||||
segment = mAudioSegmentQueue.poll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio channel is enabled.
|
||||
*/
|
||||
public boolean isDisabled()
|
||||
{
|
||||
return mDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches this channel's muted state
|
||||
* @param muted true to mute and false to unmute
|
||||
*/
|
||||
public void setMuted(boolean muted)
|
||||
{
|
||||
mMuted = muted;
|
||||
notify(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this channel is muted
|
||||
* @return muted state
|
||||
*/
|
||||
public boolean isMuted()
|
||||
{
|
||||
return mMuted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the test audio sample data at higher priority that current audio queue.
|
||||
* @param samples of test audio to play.
|
||||
*/
|
||||
public void playTest(float[] samples)
|
||||
{
|
||||
if(isDisabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mAudioBuffer.insert(samples);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this channel has an audio segment, indicating that it is right in the middle of playback and
|
||||
* the audio output should temporarily pause until more audio is available from the segment.
|
||||
* @return true if this channel is processing a non-complete audio segment.
|
||||
*/
|
||||
public boolean hasAudioSegment()
|
||||
{
|
||||
return mCurrentAudioSegment != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a null identifier collection to the UI to clear any displayed metadata (TO/FROM) identifiers
|
||||
*/
|
||||
public void clearMetadata()
|
||||
{
|
||||
if(mMetadataSent)
|
||||
{
|
||||
broadcast(null);
|
||||
mMetadataSent = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides audio for this channel from the audio segment queue.
|
||||
* @return audio or null if there are no audio segments or test audio to play.
|
||||
*/
|
||||
public float[] getAudio()
|
||||
{
|
||||
if(mAudioBuffer.isFull())
|
||||
{
|
||||
float[] audio = mAudioBuffer.get();
|
||||
|
||||
if(isMuted())
|
||||
{
|
||||
return new float[SAMPLES_PER_INTERVAL];
|
||||
}
|
||||
else
|
||||
{
|
||||
return audio;
|
||||
}
|
||||
}
|
||||
|
||||
if(mCurrentAudioSegment == null)
|
||||
{
|
||||
loadNextAudioSegment();
|
||||
}
|
||||
|
||||
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
|
||||
while(isThrowaway(mCurrentAudioSegment))
|
||||
{
|
||||
if(mCurrentBufferIndex > 0)
|
||||
{
|
||||
//Flush the buffer and insert the drop tone
|
||||
mAudioBuffer.replace(mAudioSegmentDropTone);
|
||||
}
|
||||
|
||||
disposeCurrentAudioSegment();
|
||||
loadNextAudioSegment();
|
||||
}
|
||||
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
mNoAudioFromSegmentIntervalCount++;
|
||||
}
|
||||
|
||||
while(!mAudioBuffer.isFull() && mCurrentAudioSegment != null &&
|
||||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
|
||||
{
|
||||
if(mCurrentBufferIndex < 0)
|
||||
{
|
||||
mAudioBuffer.add(mAudioSegmentStartTone);
|
||||
mAudioBuffer.add(mCurrentAudioSegment.getAudioBuffer(0));
|
||||
mCurrentBufferIndex = 1;
|
||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||
mMetadataSent = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
mAudioBuffer.add(mCurrentAudioSegment.getAudioBuffer(mCurrentBufferIndex++));
|
||||
}
|
||||
|
||||
mNoAudioFromSegmentIntervalCount = 0;
|
||||
}
|
||||
|
||||
//Dispose completed/fully-played audio segments and segments that have stalled/failed to progress
|
||||
if(mCurrentAudioSegment != null && ((mNoAudioFromSegmentIntervalCount >= 6) ||
|
||||
(mCurrentAudioSegment.isComplete() && (mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))))
|
||||
{
|
||||
disposeCurrentAudioSegment();
|
||||
mNoAudioFromSegmentIntervalCount = 0;
|
||||
}
|
||||
|
||||
float[] audio = null;
|
||||
|
||||
if(mAudioBuffer.isFull())
|
||||
{
|
||||
audio = mAudioBuffer.get();
|
||||
}
|
||||
else if(mCurrentAudioSegment == null && !mAudioBuffer.isEmpty())
|
||||
{
|
||||
audio = mAudioBuffer.flush();
|
||||
}
|
||||
|
||||
//Finally, if we have buffer audio and we're muted, return silence
|
||||
if(isMuted() && audio != null)
|
||||
{
|
||||
audio = new float[SAMPLES_PER_INTERVAL];
|
||||
}
|
||||
|
||||
return audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies listener(s) of the audio event.
|
||||
* @param eventType to notify for this channel
|
||||
*/
|
||||
public void notify(AudioEvent.Type eventType)
|
||||
{
|
||||
broadcastAudioEvent(new AudioEvent(eventType, getChannelName()));
|
||||
}
|
||||
/**
|
||||
* Registers a single listener to receive audio start and audio stop events
|
||||
*/
|
||||
public void addAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mAudioEventBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the audio event listener
|
||||
* @param listener to remove
|
||||
*/
|
||||
public void removeAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mAudioEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an audio event to the registered listener
|
||||
*/
|
||||
private void broadcastAudioEvent(AudioEvent audioEvent)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(audioEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
|
||||
* @param audioSegment to evaluate
|
||||
* @return true if the audio should be thrown away.
|
||||
*/
|
||||
private boolean isThrowaway(AudioSegment audioSegment)
|
||||
{
|
||||
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel name
|
||||
*/
|
||||
public String getChannelName()
|
||||
{
|
||||
return mChannelName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts audio identifier collection metadata to the registered listener
|
||||
*/
|
||||
private void broadcast(IdentifierCollection identifierCollection)
|
||||
{
|
||||
if(mIdentifierCollectionListener != null)
|
||||
{
|
||||
mIdentifierCollectionListener.receive(identifierCollection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a single listener to receive the audio metadata from each
|
||||
* audio packet
|
||||
*/
|
||||
public void setIdentifierCollectionListener(Listener<IdentifierCollection> listener)
|
||||
{
|
||||
mIdentifierCollectionListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the current audio metadata listener
|
||||
*/
|
||||
public void removeAudioMetadataListener()
|
||||
{
|
||||
mIdentifierCollectionListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this audio output for disposal.
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
disposeCurrentAudioSegment();
|
||||
mAudioEventBroadcaster.clear();
|
||||
mIdentifierCollectionListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
|
||||
*/
|
||||
@Subscribe
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
{
|
||||
updateToneInsertionAudioClips();
|
||||
}
|
||||
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
|
||||
{
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio segment is linked to the current playback audio segment
|
||||
*/
|
||||
public boolean isLinkedTo(AudioSegment audioSegment)
|
||||
{
|
||||
return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive and process audio identifier update notifications.
|
||||
*/
|
||||
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
|
||||
{
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection();
|
||||
|
||||
if(identifierCollection != null)
|
||||
{
|
||||
broadcast(identifierCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates audio segment start, drop and preempt tone insertion clips
|
||||
*/
|
||||
private void updateToneInsertionAudioClips()
|
||||
{
|
||||
mAudioSegmentStartTone = mUserPreferences.getPlaybackPreference().getStartTone();
|
||||
mAudioSegmentDropTone = mUserPreferences.getPlaybackPreference().getDropTone();;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
|
||||
* @return true if empty
|
||||
*/
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return mAudioSegmentQueue.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
|
||||
* calling entity.
|
||||
*
|
||||
* @param audioSegment to schedule for playback.
|
||||
*/
|
||||
public void play(AudioSegment audioSegment)
|
||||
{
|
||||
if(audioSegment != null)
|
||||
{
|
||||
//Audio segment user count has already been incremented by the external caller.
|
||||
if(isDisabled())
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
mAudioSegmentQueue.add(audioSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the currently assigned audio segment.
|
||||
*/
|
||||
private void disposeCurrentAudioSegment()
|
||||
{
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
mCurrentAudioSegment.decrementConsumerCount();
|
||||
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
|
||||
mCurrentAudioSegment = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the next audio segment from the queue.
|
||||
*/
|
||||
private void loadNextAudioSegment()
|
||||
{
|
||||
AudioSegment audioSegment = mAudioSegmentQueue.poll();
|
||||
|
||||
boolean verificationInProgress = (audioSegment != null);
|
||||
|
||||
while(verificationInProgress)
|
||||
{
|
||||
if(audioSegment != null)
|
||||
{
|
||||
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
|
||||
if(isThrowaway(audioSegment))
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment = mAudioSegmentQueue.poll();
|
||||
}
|
||||
else
|
||||
{
|
||||
verificationInProgress = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
verificationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentAudioSegment = audioSegment;
|
||||
|
||||
if(audioSegment != null)
|
||||
{
|
||||
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||
}
|
||||
|
||||
mCurrentBufferIndex = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages a stream of float PCM audio samples. Employs thread locking of the audio buffer to support both the
|
||||
* audio processor thread and user initiated test audio replay.
|
||||
*/
|
||||
static class AudioBuffer
|
||||
{
|
||||
private final Lock mLock = new ReentrantLock();
|
||||
private float[] mBuffer = new float[0];
|
||||
|
||||
/**
|
||||
* Indicates if the buffer has at least one interval of samples available
|
||||
*/
|
||||
public boolean isFull()
|
||||
{
|
||||
return mBuffer.length >= SAMPLES_PER_INTERVAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the buffer is empty
|
||||
*/
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return mBuffer.length == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the audio to the buffer
|
||||
* @param audio to add
|
||||
*/
|
||||
public void add(float[] audio)
|
||||
{
|
||||
if(audio != null)
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mBuffer = concatenate(mBuffer, audio);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the audio to the front of the buffer. This can be used to insert test audio ahead of any queued audio.
|
||||
* @param audio to insert
|
||||
*/
|
||||
public void insert(float[] audio)
|
||||
{
|
||||
if(audio != null)
|
||||
{
|
||||
mLock.lock();
|
||||
try
|
||||
{
|
||||
mBuffer = concatenate(audio, mBuffer);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the content in the buffer with the audio argument
|
||||
* @param audio to replace into the buffer
|
||||
*/
|
||||
public void replace(float[] audio)
|
||||
{
|
||||
if(audio != null)
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mBuffer = audio;
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenate two arrays. Note: this does not null check either array.
|
||||
* @param a first array
|
||||
* @param b second array
|
||||
* @return concatenated array
|
||||
*/
|
||||
private static float[] concatenate(float[] a, float[] b)
|
||||
{
|
||||
a = Arrays.copyOf(a, a.length + b.length);
|
||||
System.arraycopy(b, 0, a, a.length - b.length, b.length);
|
||||
return a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an array of 160 samples if available, otherwise returns null.
|
||||
* @return 160 samples or null.
|
||||
*/
|
||||
public float[] get()
|
||||
{
|
||||
if(mBuffer.length >= SAMPLES_PER_INTERVAL)
|
||||
{
|
||||
mLock.lock();
|
||||
try
|
||||
{
|
||||
float[] fragment = Arrays.copyOf(mBuffer, SAMPLES_PER_INTERVAL);
|
||||
mBuffer = Arrays.copyOfRange(mBuffer, SAMPLES_PER_INTERVAL, mBuffer.length);
|
||||
return fragment;
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes remaining samples from the buffer when the quantity is less than a full sample set.
|
||||
* @return remaining samples or null.
|
||||
*/
|
||||
public float[] flush()
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(mBuffer.length > 0)
|
||||
{
|
||||
float[] fragment = Arrays.copyOf(mBuffer, SAMPLES_PER_INTERVAL);
|
||||
|
||||
if(mBuffer.length > SAMPLES_PER_INTERVAL)
|
||||
{
|
||||
mBuffer = Arrays.copyOfRange(mBuffer, SAMPLES_PER_INTERVAL, mBuffer.length);
|
||||
}
|
||||
else
|
||||
{
|
||||
mBuffer = new float[0];
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +14,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
@@ -40,13 +39,6 @@ import io.github.dsheirer.settings.ColorSetting;
|
||||
import io.github.dsheirer.settings.Setting;
|
||||
import io.github.dsheirer.settings.SettingChangeListener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.Color;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.Font;
|
||||
@@ -54,7 +46,17 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
|
||||
/**
|
||||
* UI to wrap an audio channel and provide display of metadata and playback state information.
|
||||
*/
|
||||
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
@@ -66,30 +68,36 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
public static final String PROPERTY_COLOR_MUTED = PROPERTY_PREFIX + "muted";
|
||||
public static final String PROPERTY_COLOR_VALUE = PROPERTY_PREFIX + "value";
|
||||
|
||||
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
|
||||
|
||||
private Color mBackgroundColor;
|
||||
private Color mLabelColor;
|
||||
private Color mMutedColor;
|
||||
private Color mValueColor;
|
||||
|
||||
private IconModel mIconModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
private UserPreferences mUserPreferences;
|
||||
private TalkgroupFormatPreference mTalkgroupFormatPreference;
|
||||
private AudioOutput mAudioOutput;
|
||||
|
||||
private JLabel mMutedLabel = new JLabel("M");
|
||||
private JLabel mChannelName = new JLabel(" ");
|
||||
private JLabel mIconLabel = new JLabel(" ");
|
||||
private JLabel mIdentifierLabel = new JLabel("-----");
|
||||
private final AudioChannel mAudioChannel;
|
||||
private final AliasModel mAliasModel;
|
||||
private final IconModel mIconModel;
|
||||
private final SettingsManager mSettingsManager;
|
||||
private final UserPreferences mUserPreferences;
|
||||
private final TalkgroupFormatPreference mTalkgroupFormatPreference;
|
||||
private Identifier mIdentifier;
|
||||
private List<Alias> mAliases = Collections.EMPTY_LIST;
|
||||
private AliasModel mAliasModel;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
private final Lock mLock = new ReentrantLock();
|
||||
|
||||
public AudioChannelPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
AudioOutput audioOutput, AliasModel aliasModel)
|
||||
private final Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
|
||||
private final Color mBackgroundColor;
|
||||
private final Color mLabelColor;
|
||||
private final Color mMutedColor;
|
||||
private final Color mValueColor;
|
||||
private final JLabel mMutedLabel = new JLabel("M");
|
||||
private JLabel mChannelName = new JLabel(" ");
|
||||
private final JLabel mIconLabel = new JLabel(" ");
|
||||
private final JLabel mIdentifierLabel = new JLabel("-----");
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param audioChannel to wrap by this panel
|
||||
* @param aliasModel for alias lookup
|
||||
* @param iconModel for icon lookup
|
||||
* @param settingsManager for monitoring changes to tone insertion
|
||||
* @param userPreferences for lookup of tone and other preferences
|
||||
*/
|
||||
public AudioChannelPanel(AudioChannel audioChannel, AliasModel aliasModel, IconModel iconModel,
|
||||
SettingsManager settingsManager, UserPreferences userPreferences)
|
||||
{
|
||||
mIconModel = iconModel;
|
||||
mSettingsManager = settingsManager;
|
||||
@@ -97,12 +105,12 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
mAliasModel = aliasModel;
|
||||
mUserPreferences = userPreferences;
|
||||
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
|
||||
mAudioOutput = audioOutput;
|
||||
mAudioChannel = audioChannel;
|
||||
|
||||
if(mAudioOutput != null)
|
||||
if(mAudioChannel != null)
|
||||
{
|
||||
mAudioOutput.addAudioEventListener(this);
|
||||
mAudioOutput.setIdentifierCollectionListener(new AudioMetadataProcessor());
|
||||
mAudioChannel.addAudioEventListener(this);
|
||||
mAudioChannel.setIdentifierCollectionListener(new AudioMetadataProcessor());
|
||||
}
|
||||
|
||||
mBackgroundColor = SystemProperties.getInstance().get(PROPERTY_COLOR_BACKGROUND, Color.BLACK);
|
||||
@@ -131,10 +139,10 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
//Deregister from receiving preference update notifications
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
|
||||
if(mAudioOutput != null)
|
||||
if(mAudioChannel != null)
|
||||
{
|
||||
mAudioOutput.removeAudioEventListener(this);
|
||||
mAudioOutput.removeAudioMetadataListener();
|
||||
mAudioChannel.removeAudioEventListener(this);
|
||||
mAudioChannel.removeAudioMetadataListener();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +160,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
mMutedLabel.setVisible(false);
|
||||
add(mMutedLabel);
|
||||
|
||||
mChannelName = new JLabel(mAudioOutput != null ? mAudioOutput.getChannelName() : " ");
|
||||
mChannelName = new JLabel(mAudioChannel != null ? mAudioChannel.getChannelName() : " ");
|
||||
mChannelName.setFont(mFont);
|
||||
mChannelName.setForeground(mLabelColor);
|
||||
add(mChannelName);
|
||||
@@ -172,25 +180,11 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
switch(audioEvent.getType())
|
||||
{
|
||||
case AUDIO_STOPPED:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
resetLabels();
|
||||
}
|
||||
});
|
||||
EventQueue.invokeLater(this::resetLabels);
|
||||
break;
|
||||
case AUDIO_MUTED:
|
||||
case AUDIO_UNMUTED:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
mMutedLabel.setVisible(mAudioOutput.isMuted());
|
||||
}
|
||||
});
|
||||
EventQueue.invokeLater(() -> mMutedLabel.setVisible(mAudioChannel.isMuted()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,7 +14,7 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
@@ -24,18 +23,26 @@ import io.github.dsheirer.audio.IAudioController;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JSeparator;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Displays one or more audio channel panels.
|
||||
*/
|
||||
public class AudioChannelsPanel extends JPanel
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param iconModel for icon access
|
||||
* @param userPreferences to monitor for audio playback changes
|
||||
* @param settingsManager for tone insertion settings
|
||||
* @param controller for the audio channels
|
||||
* @param aliasModel for accessing aliases
|
||||
*/
|
||||
public AudioChannelsPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
IAudioController controller, AliasModel aliasModel)
|
||||
{
|
||||
@@ -44,15 +51,13 @@ public class AudioChannelsPanel extends JPanel
|
||||
|
||||
setBackground(Color.BLACK);
|
||||
|
||||
List<AudioOutput> outputs = controller.getAudioOutputs();
|
||||
|
||||
addSeparator();
|
||||
|
||||
for(int x = 0; x < outputs.size(); x++)
|
||||
for(int x = 0; x < controller.getAudioChannels().size(); x++)
|
||||
{
|
||||
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, outputs.get(x), aliasModel));
|
||||
add(new AudioChannelPanel(controller.getAudioChannels().get(x), aliasModel, iconModel, settingsManager, userPreferences));
|
||||
|
||||
if(x < outputs.size() - 1)
|
||||
if(x < controller.getAudioChannels().size() - 1)
|
||||
{
|
||||
addSeparator();
|
||||
}
|
||||
@@ -60,10 +65,10 @@ public class AudioChannelsPanel extends JPanel
|
||||
|
||||
/* Add an empty channel panel so that the panel is sized appropriately
|
||||
* for either a single channel or two channels */
|
||||
if(outputs.size() == 1)
|
||||
if(controller.getAudioChannels().size() == 1)
|
||||
{
|
||||
addSeparator();
|
||||
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, null, aliasModel), "growx");
|
||||
add(new AudioChannelPanel(null, aliasModel, iconModel, settingsManager, userPreferences), "growx");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +86,9 @@ public class AudioChannelsPanel extends JPanel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a separator to this panel.
|
||||
*/
|
||||
private void addSeparator()
|
||||
{
|
||||
JSeparator separator = new JSeparator(JSeparator.VERTICAL);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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,22 +18,14 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.controller.NamingThreadFactory;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.preference.playback.PlayTestAudioRequest;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -41,11 +33,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.BooleanControl;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.Control;
|
||||
import javax.sound.sampled.DataLine;
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import javax.sound.sampled.Line;
|
||||
import javax.sound.sampled.LineEvent;
|
||||
import javax.sound.sampled.LineListener;
|
||||
import javax.sound.sampled.LineUnavailableException;
|
||||
@@ -56,418 +47,214 @@ import javax.sound.sampled.SourceDataLine;
|
||||
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
|
||||
* and broadcasts audio segment metadata to registered listeners (ie gui components).
|
||||
*/
|
||||
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
|
||||
public class AudioOutput implements LineListener
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
|
||||
private int mBufferStartThreshold;
|
||||
private int mBufferStopThreshold;
|
||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||
private SourceDataLine mOutput;
|
||||
private Mixer mMixer;
|
||||
private MixerChannel mMixerChannel;
|
||||
private static final int BUFFER_SIZE_SAMPLES = 1000; //At 8 kHz audio
|
||||
private final AudioPlaybackDeviceDescriptor mAudioPlaybackDeviceDescriptor;
|
||||
private final AudioProvider mAudioProvider;
|
||||
private FloatControl mGainControl;
|
||||
private BooleanControl mMuteControl;
|
||||
private AudioEvent mAudioStartEvent;
|
||||
private AudioEvent mAudioStopEvent;
|
||||
private boolean mCanProcessAudio = false;
|
||||
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private AudioSegment mCurrentAudioSegment;
|
||||
private int mCurrentBufferIndex = 0;
|
||||
private UserPreferences mUserPreferences;
|
||||
private ByteBuffer mAudioSegmentStartTone;
|
||||
private ByteBuffer mAudioSegmentDropTone;
|
||||
private boolean mRunning = false;
|
||||
private ScheduledExecutorService mScheduledExecutorService;
|
||||
private ScheduledFuture<?> mProcessorFuture;
|
||||
private boolean mDropDuplicates;
|
||||
private long mOutputLastTimestamp = 0;
|
||||
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
|
||||
private AudioFormat mAudioFormat;
|
||||
private Line.Info mLineInfo;
|
||||
private int mRequestedBufferSize;
|
||||
private SourceDataLine mSourceDataLine;
|
||||
private boolean mCanProcessAudio = false;
|
||||
private boolean mRunning = false;
|
||||
|
||||
|
||||
/**
|
||||
* Single audio channel playback with automatic starting and stopping of the
|
||||
* underlying sourcedataline specified by the mixer and mixer channel
|
||||
* arguments.
|
||||
* Audio output for the selected audio playback device and provider. Opens a SourceDataLine from the miser for the
|
||||
* audio format specified in the descriptor. Employs a single threaded executor to process audio 10x a second and
|
||||
* manages the start/stop control for the dataline based on audio availability.
|
||||
*
|
||||
* Maintains an internal non-blocking audio packet queue and processes this
|
||||
* queue 25 times a second (every 40 ms).
|
||||
*
|
||||
* @param mixer to obtain source data line
|
||||
* @param mixerChannel either mono or left/right stereo
|
||||
* @param audioFormat to use during playback
|
||||
* @param lineInfo to use when obtaining the source data line
|
||||
* @param requestedBufferSize of approximately 1 second of audio
|
||||
* @param descriptor for the mixer and audio format
|
||||
* @param audioProvider for access to audio from audio segments
|
||||
*/
|
||||
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
|
||||
int requestedBufferSize, UserPreferences userPreferences)
|
||||
public AudioOutput(AudioPlaybackDeviceDescriptor descriptor, AudioProvider audioProvider)
|
||||
{
|
||||
mMixer = mixer;
|
||||
mMixerChannel = mixerChannel;
|
||||
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
||||
"sdrtrunk audio output " + mixerChannel.name()));
|
||||
mUserPreferences = userPreferences;
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mAudioFormat = audioFormat;
|
||||
mLineInfo = lineInfo;
|
||||
mRequestedBufferSize = requestedBufferSize;
|
||||
mAudioProvider = audioProvider;
|
||||
Mixer mixer = AudioSystem.getMixer(descriptor.getMixerInfo());
|
||||
|
||||
try
|
||||
if(mixer == null)
|
||||
{
|
||||
mOutput = (SourceDataLine) mMixer.getLine(mLineInfo);
|
||||
List<AudioPlaybackDeviceDescriptor> descriptors = AudioPlaybackDeviceManager.getAudioPlaybackDevices();
|
||||
|
||||
if(mOutput != null)
|
||||
int selected = 0;
|
||||
|
||||
while((mixer == null) && (selected < descriptors.size()))
|
||||
{
|
||||
mOutput.open(audioFormat, requestedBufferSize);
|
||||
descriptor = descriptors.get(selected++);
|
||||
mixer = AudioSystem.getMixer(descriptor.getMixerInfo());
|
||||
}
|
||||
}
|
||||
|
||||
//Start threshold: buffer is full with 10% or less of capacity remaining
|
||||
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
|
||||
mAudioPlaybackDeviceDescriptor = descriptor;
|
||||
|
||||
//Stop threshold: buffer is empty with 90% or more capacity available
|
||||
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
|
||||
if(mixer != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
mSourceDataLine = (SourceDataLine) mixer.getLine(new DataLine.Info(SourceDataLine.class,
|
||||
descriptor.getAudioFormat()));
|
||||
|
||||
mOutput.addLineListener(this);
|
||||
|
||||
if(mOutput != null)
|
||||
if(mSourceDataLine != null)
|
||||
{
|
||||
mSourceDataLine.addLineListener(this);
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
Control gain = mSourceDataLine.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 [" + mixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + "]");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
|
||||
"MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
}
|
||||
|
||||
//Run the queue processor task every 100 milliseconds or 10 times a second
|
||||
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
mLog.error("Couldn't open source data line for mixer: " + descriptor.getMixerInfo().getName(), e);
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
||||
mCanProcessAudio = true;
|
||||
if(mCanProcessAudio)
|
||||
{
|
||||
openSourceDataLine();
|
||||
|
||||
//The audio provider gives us audio 160 samples per interval representing 20 milliseconds of audio.
|
||||
// Run the scheduled executor at just slightly faster than that time, recognizing that it will
|
||||
// block against the mixer source data line until it can write all 160 samples.
|
||||
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
||||
"sdrtrunk audio output " + descriptor.getMixerInfo().getName()));
|
||||
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioProcessor(),
|
||||
0, 19, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Disable each of the audio channels so they don't queue audio segments.
|
||||
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||
{
|
||||
audioChannel.setDisabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens and starts the source data line.
|
||||
*/
|
||||
private void openSourceDataLine()
|
||||
{
|
||||
try
|
||||
{
|
||||
if(mSourceDataLine != null && !mSourceDataLine.isOpen())
|
||||
{
|
||||
int bufferSize = BUFFER_SIZE_SAMPLES * 2 * mAudioPlaybackDeviceDescriptor.getAudioFormat().getChannels();
|
||||
mSourceDataLine.open(mAudioProvider.getAudioFormat(), bufferSize);
|
||||
bufferSize = mSourceDataLine.getBufferSize();
|
||||
//Fill the line with silence so we can start it
|
||||
mSourceDataLine.write(new byte[bufferSize], 0, bufferSize);
|
||||
mSourceDataLine.start();
|
||||
|
||||
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||
{
|
||||
audioChannel.setDisabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
}
|
||||
LOGGING_SUPPRESSOR.error("Can't open", 3, "Error opening source data line", e);
|
||||
mRunning = false;
|
||||
|
||||
updateToneInsertionAudioClips();
|
||||
|
||||
//Register to receive preference update notifications so we can update the preference items
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
|
||||
* @return true if empty
|
||||
*/
|
||||
public boolean isEmpty()
|
||||
{
|
||||
return mAudioSegmentQueue.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
|
||||
* calling entity.
|
||||
*
|
||||
* @param audioSegment to schedule for playback.
|
||||
*/
|
||||
public void play(AudioSegment audioSegment)
|
||||
{
|
||||
if(audioSegment != null)
|
||||
{
|
||||
//Audio segment use count has already been incremented by the external caller.
|
||||
mAudioSegmentQueue.add(audioSegment);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio segment is linked to the current playback audio segment
|
||||
*/
|
||||
public boolean isLinkedTo(AudioSegment audioSegment)
|
||||
{
|
||||
return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive and process audio identifier update notifications.
|
||||
*/
|
||||
@Override
|
||||
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
|
||||
{
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection();
|
||||
|
||||
if(identifierCollection != null)
|
||||
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||
{
|
||||
broadcast(identifierCollection);
|
||||
audioChannel.setDisabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
|
||||
* Plays the test audio over the selected audio channel(s)
|
||||
* @param request to play test audio
|
||||
*/
|
||||
@Subscribe
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
public void playTestAudio(PlayTestAudioRequest request)
|
||||
{
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
//If we're not running, resize the audio samples to fill the buffer enough to trigger playback.
|
||||
float[] audio = mRunning ? request.audio() : Arrays.copyOf(request.audio(), BUFFER_SIZE_SAMPLES);
|
||||
|
||||
List<AudioChannel> channels = mAudioProvider.getAudioChannels();
|
||||
|
||||
if(request.isAllChannels())
|
||||
{
|
||||
updateToneInsertionAudioClips();
|
||||
}
|
||||
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
|
||||
{
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates audio segment start, drop and preempt tone insertion clips
|
||||
*/
|
||||
private void updateToneInsertionAudioClips()
|
||||
{
|
||||
mAudioSegmentStartTone = null;
|
||||
mAudioSegmentDropTone = null;
|
||||
|
||||
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
|
||||
|
||||
if(start != null)
|
||||
{
|
||||
mAudioSegmentStartTone = convert(start);
|
||||
}
|
||||
|
||||
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
|
||||
|
||||
if(drop != null)
|
||||
{
|
||||
mAudioSegmentDropTone = convert(drop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the audio buffer data to the source data line. If the data line is not currently playing, write as
|
||||
* much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual
|
||||
* buffer content to the data line as a blocking call.
|
||||
*
|
||||
* @param buffer of audio to playback
|
||||
*/
|
||||
private void playAudio(ByteBuffer buffer)
|
||||
{
|
||||
if(mOutput == null)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request");
|
||||
return;
|
||||
}
|
||||
|
||||
if(buffer != null && buffer.array().length > 0)
|
||||
{
|
||||
int wrote = 0;
|
||||
|
||||
//If the output data line is not running, we can only write up to the available capacity. So, only write
|
||||
//what will fit initially, start playback, and then use a blocking write for the remainder.
|
||||
if(!mOutput.isRunning())
|
||||
for(AudioChannel channel : channels)
|
||||
{
|
||||
int toWrite = mOutput.available();
|
||||
|
||||
if(toWrite > buffer.array().length)
|
||||
{
|
||||
toWrite = buffer.array().length;
|
||||
}
|
||||
|
||||
//Top off the buffer and check if we can start it
|
||||
wrote += mOutput.write(buffer.array(), 0, toWrite);
|
||||
mOutputLastTimestamp = System.currentTimeMillis();
|
||||
|
||||
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. 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.open();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error after closing and attempting to reopen audio output", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mOutput.isRunning() && wrote < buffer.array().length)
|
||||
{
|
||||
//This will block until the buffer is fully written to the data line
|
||||
mOutput.write(buffer.array(), wrote, buffer.array().length - wrote);
|
||||
mOutputLastTimestamp = System.currentTimeMillis();
|
||||
channel.playTest(audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose of the currently assigned audio segment.
|
||||
*/
|
||||
private void disposeCurrentAudioSegment()
|
||||
{
|
||||
if(mCurrentAudioSegment != null)
|
||||
else if(request.channel() < channels.size())
|
||||
{
|
||||
mCurrentAudioSegment.decrementConsumerCount();
|
||||
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
|
||||
mCurrentAudioSegment = null;
|
||||
broadcast(null);
|
||||
channels.get(request.channel()).playTest(audio);
|
||||
}
|
||||
else if(!channels.isEmpty())
|
||||
{
|
||||
channels.getFirst().playTest(audio);
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGING_SUPPRESSOR.info("No Audio Channels", 2,
|
||||
"Unable to play test audio - no audio channels configured currently");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the next audio segment from the queue.
|
||||
* Audio playback device descriptor for this audio output
|
||||
*/
|
||||
private void loadNextAudioSegment()
|
||||
public AudioPlaybackDeviceDescriptor getAudioPlaybackDeviceDescriptor()
|
||||
{
|
||||
AudioSegment audioSegment = mAudioSegmentQueue.poll();
|
||||
|
||||
boolean verificationInProgress = (audioSegment != null);
|
||||
|
||||
while(verificationInProgress)
|
||||
{
|
||||
if(audioSegment != null)
|
||||
{
|
||||
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
|
||||
if(isThrowaway(audioSegment))
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment = mAudioSegmentQueue.poll();
|
||||
}
|
||||
else
|
||||
{
|
||||
verificationInProgress = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
verificationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentAudioSegment = audioSegment;
|
||||
|
||||
if(audioSegment != null)
|
||||
{
|
||||
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||
}
|
||||
|
||||
mCurrentBufferIndex = 0;
|
||||
return mAudioPlaybackDeviceDescriptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
|
||||
* @param audioSegment to evaluate
|
||||
* @return true if the audio should be thrown away.
|
||||
* Audio provider managed by this audio output
|
||||
*/
|
||||
private boolean isThrowaway(AudioSegment audioSegment)
|
||||
public AudioProvider getAudioProvider()
|
||||
{
|
||||
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
|
||||
return mAudioProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
|
||||
* by a threaded processor repeatedly to playback the current audio segment and check for and start newly
|
||||
* added audio segments. It also handles starting and stopping the playback source data line to avoid audio
|
||||
* discontinuities due to buffer underruns.
|
||||
* Process audio from the audio provider and writes it to the mixer's source data line. Calls to this method can
|
||||
* block on the source data line until it has capacity available to accept the audio.
|
||||
*/
|
||||
private void processAudio()
|
||||
{
|
||||
if(mCurrentAudioSegment == null)
|
||||
if(mSourceDataLine == null)
|
||||
{
|
||||
loadNextAudioSegment();
|
||||
LOGGING_SUPPRESSOR.error("null output", 2,
|
||||
"Audio Output source data line is null - ignoring audio playback request");
|
||||
return;
|
||||
}
|
||||
|
||||
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
|
||||
while(isThrowaway(mCurrentAudioSegment))
|
||||
ByteBuffer buffer = mAudioProvider.getAudio();
|
||||
|
||||
if(buffer != null)
|
||||
{
|
||||
if(mCurrentBufferIndex > 0)
|
||||
//This is a blocking method call.
|
||||
int wrote = mSourceDataLine.write(buffer.array(), 0, buffer.array().length);
|
||||
|
||||
//Around JDK 22 something started causing the source data line to fail to accept audio byte data via the
|
||||
//write() method. When this happens, close and reopen the data line to clear the error state.
|
||||
if(wrote <= 0)
|
||||
{
|
||||
playAudio(mAudioSegmentDropTone);
|
||||
}
|
||||
|
||||
disposeCurrentAudioSegment();
|
||||
loadNextAudioSegment();
|
||||
}
|
||||
|
||||
while(mCurrentAudioSegment != null && mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
|
||||
{
|
||||
//Continuously evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
|
||||
if(isThrowaway(mCurrentAudioSegment))
|
||||
{
|
||||
if(mCurrentBufferIndex > 0)
|
||||
{
|
||||
playAudio(mAudioSegmentDropTone);
|
||||
}
|
||||
|
||||
disposeCurrentAudioSegment();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mCurrentBufferIndex == 0)
|
||||
{
|
||||
playAudio(mAudioSegmentStartTone);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
|
||||
|
||||
if(audioBuffer != null)
|
||||
{
|
||||
ByteBuffer audio = convert(audioBuffer);
|
||||
//This call blocks until all audio bytes are dumped into the data line.
|
||||
playAudio(audio);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error while processing audio for [" + mMixerChannel.name() + "]", e);
|
||||
}
|
||||
LOGGING_SUPPRESSOR.info("Stalled Data Line", 3,
|
||||
"Audio playback data line has stopped accepting samples - recycling to clear the error");
|
||||
mSourceDataLine.close();
|
||||
openSourceDataLine();
|
||||
}
|
||||
}
|
||||
|
||||
//Check for completed and fully-played audio segment to closeout
|
||||
if(mCurrentAudioSegment != null &&
|
||||
mCurrentAudioSegment.isComplete() &&
|
||||
(mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
|
||||
{
|
||||
disposeCurrentAudioSegment();
|
||||
}
|
||||
|
||||
checkStop();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -475,156 +262,30 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
mCanProcessAudio = false;
|
||||
|
||||
if(mProcessorFuture != null)
|
||||
{
|
||||
mProcessorFuture.cancel(true);
|
||||
}
|
||||
|
||||
mProcessorFuture = null;
|
||||
disposeCurrentAudioSegment();
|
||||
mAudioEventBroadcaster.clear();
|
||||
mIdentifierCollectionListener = null;
|
||||
|
||||
if(mOutput != null)
|
||||
if(mScheduledExecutorService != null)
|
||||
{
|
||||
mOutput.close();
|
||||
mScheduledExecutorService.shutdownNow();
|
||||
mScheduledExecutorService = null;
|
||||
}
|
||||
|
||||
mOutput = null;
|
||||
mAudioProvider.dispose();
|
||||
mCanProcessAudio = false;
|
||||
|
||||
if(mSourceDataLine != null)
|
||||
{
|
||||
mSourceDataLine.stop();
|
||||
mSourceDataLine.flush();
|
||||
mSourceDataLine.close();
|
||||
}
|
||||
mSourceDataLine = null;
|
||||
mGainControl = null;
|
||||
mMuteControl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line.
|
||||
*/
|
||||
protected abstract ByteBuffer convert(float[] buffer);
|
||||
|
||||
/**
|
||||
* Audio output channel name
|
||||
*/
|
||||
public String getChannelName()
|
||||
{
|
||||
return mMixerChannel.getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixer Channel for this audio output
|
||||
*/
|
||||
protected MixerChannel getMixerChannel()
|
||||
{
|
||||
return mMixerChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a single listener to receive audio start and audio stop events
|
||||
*/
|
||||
public void addAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mAudioEventBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
public void removeAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mAudioEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts an audio event to the registered listener
|
||||
*/
|
||||
private void broadcastAudioEvent(AudioEvent audioEvent)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(audioEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a single listener to receive the audio metadata from each
|
||||
* audio packet
|
||||
*/
|
||||
public void setIdentifierCollectionListener(Listener<IdentifierCollection> listener)
|
||||
{
|
||||
mIdentifierCollectionListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the current audio metadata listener
|
||||
*/
|
||||
public void removeAudioMetadataListener()
|
||||
{
|
||||
mIdentifierCollectionListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts audio identifier collection metadata to the registered listener
|
||||
*/
|
||||
private void broadcast(IdentifierCollection identifierCollection)
|
||||
{
|
||||
if(mIdentifierCollectionListener != null)
|
||||
{
|
||||
mIdentifierCollectionListener.receive(identifierCollection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts audio playback once audio buffer is almost full and remaining capacity falls below the start threshold.
|
||||
*
|
||||
* Note: this method should only be invoked from the processAudio() method
|
||||
*/
|
||||
private void checkStart()
|
||||
{
|
||||
if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold)
|
||||
{
|
||||
mOutput.start();
|
||||
mRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops audio playback and drains the audio buffer to empty when the audio buffer is mostly empty and the
|
||||
* available buffer capacity exceeds the stop threshold
|
||||
*
|
||||
* Note: this method should only be invoked from the processAudio() method
|
||||
*/
|
||||
private void checkStop()
|
||||
{
|
||||
if(mRunning)
|
||||
{
|
||||
//If the output buffer falls below the threshold then drain the output and stop playback
|
||||
if(mOutput.isRunning())
|
||||
{
|
||||
if(mOutput.available() >= mBufferStopThreshold)
|
||||
{
|
||||
mOutput.drain();
|
||||
mOutput.stop();
|
||||
mRunning = false;
|
||||
}
|
||||
//Detect when we have a partially filled output that will neither start nor stop and the last write
|
||||
//timestamp was more than the staleness threshold (500 ms) -- stop the output and discard stale contents
|
||||
else if(mOutput.available() < mBufferStopThreshold &&
|
||||
mOutput.available() > mBufferStartThreshold &&
|
||||
(System.currentTimeMillis() - mOutputLastTimestamp >= STALE_PLAYBACK_THRESHOLD_MS))
|
||||
{
|
||||
mOutput.stop();
|
||||
//Discard the stale buffer contents.
|
||||
mOutput.flush();
|
||||
mRunning = false;
|
||||
}
|
||||
}
|
||||
//If output playback stopped on its own because the buffer emptied, then cleanup
|
||||
else
|
||||
{
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
//If we stopped audio playback, broadcast a null identifier to clear the gui panel
|
||||
if(!mRunning)
|
||||
{
|
||||
broadcast(null);
|
||||
}
|
||||
}
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -632,11 +293,13 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
*/
|
||||
public void setMuted(boolean muted)
|
||||
{
|
||||
if(mMuteControl != null)
|
||||
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
|
||||
{
|
||||
mMuteControl.setValue(muted);
|
||||
broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
||||
audioChannel.setMuted(muted);
|
||||
}
|
||||
|
||||
AudioEvent.Type type = muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED;
|
||||
mAudioProvider.notify(type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -644,9 +307,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
*/
|
||||
public boolean isMuted()
|
||||
{
|
||||
if(mMuteControl != null)
|
||||
if(!mAudioProvider.getAudioChannels().isEmpty())
|
||||
{
|
||||
return mMuteControl.getValue();
|
||||
return mAudioProvider.getAudioChannels().getFirst().isMuted();
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -679,20 +342,20 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
|
||||
if(type == LineEvent.Type.START)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(mAudioStartEvent);
|
||||
mAudioProvider.notify(AudioEvent.Type.AUDIO_STARTED);
|
||||
}
|
||||
else if(type == LineEvent.Type.STOP)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
|
||||
mAudioProvider.notify(AudioEvent.Type.AUDIO_STOPPED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable audio segment processor
|
||||
* Audio Processor thread
|
||||
*/
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
public class AudioProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
private final AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,8 +20,6 @@ package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioException;
|
||||
import io.github.dsheirer.audio.IAudioController;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.preference.PreferenceEditorType;
|
||||
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
|
||||
@@ -29,81 +27,77 @@ import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import java.awt.Color;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import io.github.dsheirer.gui.theme.ThemedFontIcon;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.FloatControl;
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JMenu;
|
||||
import javax.swing.JMenuItem;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JPopupMenu;
|
||||
import javax.swing.JSlider;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Color;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
|
||||
/**
|
||||
* Audio playback panel
|
||||
*/
|
||||
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
|
||||
|
||||
private static ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
|
||||
private static ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
|
||||
|
||||
private IconModel mIconModel;
|
||||
private SettingsManager mSettingsManager;
|
||||
private IAudioController mController;
|
||||
private AliasModel mAliasModel;
|
||||
private UserPreferences mUserPreferences;
|
||||
|
||||
private JButton mMuteButton;
|
||||
private static final ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
|
||||
private static final ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
|
||||
private final AliasModel mAliasModel;
|
||||
private final AudioPlaybackManager mAudioPlaybackManager;
|
||||
private final IconModel mIconModel;
|
||||
private final SettingsManager mSettingsManager;
|
||||
private final UserPreferences mUserPreferences;
|
||||
private AudioChannelsPanel mAudioChannelsPanel;
|
||||
private JButton mMuteButton;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param iconModel for icon lookup
|
||||
* @param userPreferences for preference lookup
|
||||
* @param settingsManager to monitor for changes
|
||||
* @param audioPlaybackManager for accessing the audio output
|
||||
* @param aliasModel for alias lookup
|
||||
*/
|
||||
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
IAudioController controller, AliasModel aliasModel)
|
||||
AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel)
|
||||
{
|
||||
mIconModel = iconModel;
|
||||
mSettingsManager = settingsManager;
|
||||
mController = controller;
|
||||
mAudioPlaybackManager = audioPlaybackManager;
|
||||
mAliasModel = aliasModel;
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
mController.addControllerListener(this);
|
||||
|
||||
mAudioPlaybackManager.addAudioEventListener(this);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the display
|
||||
*/
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
|
||||
setBackground(Color.BLACK);
|
||||
|
||||
mMuteButton = new MuteButton();
|
||||
mMuteButton.setBackground(getBackground());
|
||||
add(mMuteButton);
|
||||
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
|
||||
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
|
||||
add(mAudioChannelsPanel);
|
||||
|
||||
addMouseListener(new MouseSelectionListener());
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive audio event notifications from the audio playback controller
|
||||
*/
|
||||
@Override
|
||||
public void receive(AudioEvent event)
|
||||
{
|
||||
@@ -112,19 +106,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
case AUDIO_CONFIGURATION_CHANGE_STARTED:
|
||||
break;
|
||||
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
remove(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.dispose();
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
|
||||
add(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.repaint();
|
||||
revalidate();
|
||||
repaint();
|
||||
}
|
||||
EventQueue.invokeLater(() -> {
|
||||
remove(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.dispose();
|
||||
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
|
||||
add(mAudioChannelsPanel);
|
||||
mAudioChannelsPanel.repaint();
|
||||
revalidate();
|
||||
repaint();
|
||||
});
|
||||
break;
|
||||
default:
|
||||
@@ -135,26 +124,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
/**
|
||||
* Audio output mute control menu item.
|
||||
*/
|
||||
public class AudioOutputMuteItem extends JMenuItem
|
||||
public static class AudioOutputMuteItem extends JMenuItem
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private AudioOutput mAudioOutput;
|
||||
private final AudioOutput mAudioOutput;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param audioOutput to mute/unmute
|
||||
*/
|
||||
public AudioOutputMuteItem(AudioOutput audioOutput)
|
||||
{
|
||||
super(audioOutput.isMuted() ? "Unmute" : "Mute");
|
||||
|
||||
mAudioOutput = audioOutput;
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
mAudioOutput.setMuted(!mAudioOutput.isMuted());
|
||||
}
|
||||
});
|
||||
addActionListener(e -> mAudioOutput.setMuted(!mAudioOutput.isMuted()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,37 +151,23 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
if(SwingUtilities.isRightMouseButton(event))
|
||||
{
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
/* Audio mixer/output selection menus */
|
||||
JMenuItem outputMenu = new JMenuItem("Configure ...");
|
||||
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
|
||||
JMenuItem outputMenu = new JMenuItem("Audio Playback Device ...");
|
||||
Icon icon = new ThemedFontIcon(FontAwesome.COG, 14);
|
||||
outputMenu.setIcon(icon);
|
||||
outputMenu.addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
|
||||
}
|
||||
});
|
||||
outputMenu.addActionListener(e -> MyEventBus.getGlobalEventBus()
|
||||
.post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT)));
|
||||
popup.add(outputMenu);
|
||||
popup.add(new JPopupMenu.Separator());
|
||||
|
||||
/* Audio output mute and volume control */
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
//Add optional gain controller for th eaudio output
|
||||
if(mAudioPlaybackManager.getAudioOutput() != null && mAudioPlaybackManager.getAudioOutput().hasGainControl())
|
||||
{
|
||||
JMenu menu = new JMenu("Channel: " + output.getChannelName());
|
||||
|
||||
menu.add(new AudioOutputMuteItem(output));
|
||||
|
||||
if(output.hasGainControl())
|
||||
{
|
||||
JMenu volume = new JMenu("Volume");
|
||||
volume.add(new VolumeSlider(output.getGainControl()));
|
||||
menu.add(volume);
|
||||
}
|
||||
|
||||
popup.add(menu);
|
||||
popup.add(new JPopupMenu.Separator());
|
||||
JMenuItem volume = new JMenuItem("Audio Volume");
|
||||
volume.setEnabled(false);
|
||||
Icon volumeIcon = new ThemedFontIcon(FontAwesome.VOLUME_UP, 14);
|
||||
volume.setIcon(volumeIcon);
|
||||
popup.add(volume);
|
||||
popup.add(new VolumeSlider(mAudioPlaybackManager.getAudioOutput().getGainControl()));
|
||||
}
|
||||
|
||||
popup.show(event.getComponent(), event.getX(), event.getY());
|
||||
@@ -230,12 +198,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
/**
|
||||
* Audio volume (gain) adjustment slider control
|
||||
*/
|
||||
public class VolumeSlider extends JSlider
|
||||
public static class VolumeSlider extends JSlider
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private FloatControl mFloatControl;
|
||||
private final FloatControl mFloatControl;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param control to be controlled
|
||||
*/
|
||||
public VolumeSlider(FloatControl control)
|
||||
{
|
||||
super(0, 100, 0);
|
||||
@@ -244,21 +214,11 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
setMinorTickSpacing(5);
|
||||
setPaintTicks(true);
|
||||
setPaintLabels(true);
|
||||
|
||||
mFloatControl = control;
|
||||
|
||||
setValue(getIntegerValue(mFloatControl.getValue()));
|
||||
|
||||
addChangeListener(new ChangeListener()
|
||||
{
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent event)
|
||||
{
|
||||
mFloatControl.shift(mFloatControl.getValue(),
|
||||
getFloatValue(VolumeSlider.this.getValue()),
|
||||
1000);
|
||||
}
|
||||
});
|
||||
addChangeListener(event -> mFloatControl.shift(mFloatControl.getValue(),
|
||||
getFloatValue(VolumeSlider.this.getValue()),
|
||||
1000));
|
||||
|
||||
addMouseListener(new MouseListener()
|
||||
{
|
||||
@@ -270,22 +230,10 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
VolumeSlider.this.setValue(50);
|
||||
}
|
||||
}
|
||||
|
||||
public void mouseReleased(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mousePressed(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mouseExited(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
|
||||
public void mouseEntered(MouseEvent arg0)
|
||||
{
|
||||
}
|
||||
public void mouseReleased(MouseEvent arg0) {}
|
||||
public void mousePressed(MouseEvent arg0) {}
|
||||
public void mouseExited(MouseEvent arg0) {}
|
||||
public void mouseEntered(MouseEvent arg0) {}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -331,52 +279,12 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixer/Channel configuration selection item
|
||||
*/
|
||||
public class MixerSelectionItem extends JMenuItem
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private MixerChannelConfiguration mConfiguration;
|
||||
|
||||
public MixerSelectionItem(MixerChannelConfiguration config)
|
||||
{
|
||||
super(config.toString());
|
||||
|
||||
mConfiguration = config;
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
mController.setMixerChannelConfiguration(mConfiguration);
|
||||
}
|
||||
catch(AudioException e1)
|
||||
{
|
||||
mLog.error("Couldn't set mixer channel configuration "
|
||||
+ "to: " + mConfiguration.toString());
|
||||
|
||||
JOptionPane.showMessageDialog(MixerSelectionItem.this,
|
||||
"Couldn't set [" + mConfiguration.toString() +
|
||||
"] as the audio output device");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute button to mute all audio output channels exposed by the audio
|
||||
* controller
|
||||
*/
|
||||
public class MuteButton extends JButton
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private boolean mMuted = false;
|
||||
|
||||
public MuteButton()
|
||||
@@ -384,29 +292,13 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
setIcon(UNMUTED_ICON);
|
||||
setBorderPainted(false);
|
||||
getAccessibleContext().setAccessibleName("Mute");
|
||||
|
||||
addActionListener(new ActionListener()
|
||||
{
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
mMuted = !mMuted;
|
||||
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
{
|
||||
output.setMuted(mMuted);
|
||||
}
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
|
||||
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
|
||||
}
|
||||
});
|
||||
}
|
||||
addActionListener(e -> {
|
||||
mMuted = !mMuted;
|
||||
mAudioPlaybackManager.getAudioOutput().setMuted(mMuted);
|
||||
EventQueue.invokeLater(() -> {
|
||||
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
|
||||
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.Mixer;
|
||||
|
||||
/**
|
||||
* Audio playback device descriptor for a mixer and a supported audio output format (ie number of channels).
|
||||
*/
|
||||
public class AudioPlaybackDeviceDescriptor implements Comparable<AudioPlaybackDeviceDescriptor>
|
||||
{
|
||||
private final Mixer.Info mMixerInfo;
|
||||
private final AudioFormat mAudioFormat;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param mixerInfo for the mixer
|
||||
* @param audioFormat supported by the mixer
|
||||
*/
|
||||
public AudioPlaybackDeviceDescriptor(Mixer.Info mixerInfo, AudioFormat audioFormat)
|
||||
{
|
||||
mMixerInfo = mixerInfo;
|
||||
mAudioFormat = audioFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return getCleanDescription(getMixerInfo()) + "(" + getMixerInfo().getName() + ") - " +
|
||||
getChannelDescription(getAudioFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans the mixer description to reformat/reword certain mixer descriptions
|
||||
* @param mixerInfo containing a description
|
||||
* @return cleaned description
|
||||
*/
|
||||
private static String getCleanDescription(Mixer.Info mixerInfo)
|
||||
{
|
||||
if(mixerInfo.getDescription().contains("Direct Audio Device: "))
|
||||
{
|
||||
return mixerInfo.getDescription().replace("Direct Audio Device: ", "");
|
||||
}
|
||||
else if(mixerInfo.getDescription().contains("alsa_playback.java"))
|
||||
{
|
||||
return "Default (Java/ALSA)";
|
||||
}
|
||||
else
|
||||
{
|
||||
return mixerInfo.getDescription();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a description of the channels for the audio format
|
||||
* @param audioFormat with channels
|
||||
* @return pretty string
|
||||
*/
|
||||
private static String getChannelDescription(AudioFormat audioFormat)
|
||||
{
|
||||
return switch(audioFormat.getChannels())
|
||||
{
|
||||
case 1 -> "Mono";
|
||||
case 2 -> "Stereo";
|
||||
case AudioSystem.NOT_SPECIFIED -> "Unknown number of channels";
|
||||
default -> audioFormat.getChannels() + " Channels";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio format supported by the mixer
|
||||
*
|
||||
* @return audio format
|
||||
*/
|
||||
public AudioFormat getAudioFormat()
|
||||
{
|
||||
return mAudioFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixer information for the available system mixer.
|
||||
*/
|
||||
public Mixer.Info getMixerInfo()
|
||||
{
|
||||
return mMixerInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(AudioPlaybackDeviceDescriptor other)
|
||||
{
|
||||
if(other == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(other.getMixerInfo().getDescription().equals(getMixerInfo().getDescription()))
|
||||
{
|
||||
return Integer.compare(getAudioFormat().getChannels(), other.getAudioFormat().getChannels());
|
||||
}
|
||||
|
||||
return getMixerInfo().getDescription().compareTo(other.getMixerInfo().getDescription());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.DataLine;
|
||||
import javax.sound.sampled.Line;
|
||||
import javax.sound.sampled.Mixer;
|
||||
|
||||
/**
|
||||
* Audio playback device manager.
|
||||
*/
|
||||
public class AudioPlaybackDeviceManager
|
||||
{
|
||||
/**
|
||||
* Always returns the first available device in the system.
|
||||
*/
|
||||
public static AudioPlaybackDeviceDescriptor getDefaultAudioPLaybackDevice()
|
||||
{
|
||||
List<AudioPlaybackDeviceDescriptor> devices = getAudioPlaybackDevices();
|
||||
|
||||
if(!devices.isEmpty())
|
||||
{
|
||||
return devices.getFirst();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the matching audio playback device
|
||||
* @param name of the device
|
||||
* @param channels count
|
||||
* @return matching entry or the default playback device.
|
||||
*/
|
||||
public static AudioPlaybackDeviceDescriptor getAudioPlaybackDevice(String name, int channels)
|
||||
{
|
||||
for(AudioPlaybackDeviceDescriptor device: getAudioPlaybackDevices())
|
||||
{
|
||||
if(device.getMixerInfo().getName().equals(name) && device.getAudioFormat().getChannels() == channels)
|
||||
{
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultAudioPLaybackDevice();
|
||||
}
|
||||
|
||||
public static List<AudioPlaybackDeviceDescriptor> getAudioPlaybackDevices()
|
||||
{
|
||||
List<AudioPlaybackDeviceDescriptor> devices = new ArrayList<>();
|
||||
|
||||
Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
|
||||
|
||||
for(Mixer.Info mixerInfo : mixerInfos)
|
||||
{
|
||||
Mixer mixer = AudioSystem.getMixer(mixerInfo);
|
||||
|
||||
if(mixer != null)
|
||||
{
|
||||
List<AudioFormat> discoveredFormats = new ArrayList<>();
|
||||
|
||||
for(Line.Info sourceLineInfo : mixer.getSourceLineInfo())
|
||||
{
|
||||
if(sourceLineInfo instanceof DataLine.Info dataLineInfo)
|
||||
{
|
||||
for(AudioFormat audioFormat : dataLineInfo.getFormats())
|
||||
{
|
||||
if(!audioFormat.isBigEndian() && audioFormat.getSampleSizeInBits() == 16 &&
|
||||
!discoveredFormats.contains(audioFormat))
|
||||
{
|
||||
if(audioFormat.getChannels() == 1)
|
||||
{
|
||||
if(audioFormat.getSampleRate() == AudioSystem.NOT_SPECIFIED)
|
||||
{
|
||||
discoveredFormats.add(audioFormat);
|
||||
//Modify the format for 8 kHz sample rate
|
||||
AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
||||
8000, 16, audioFormat.getChannels(), audioFormat.getFrameSize(),
|
||||
8000, false);
|
||||
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
|
||||
}
|
||||
else if(audioFormat.getSampleRate() == 8000)
|
||||
{
|
||||
discoveredFormats.add(audioFormat);
|
||||
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, audioFormat));
|
||||
}
|
||||
}
|
||||
//Stereo and 64-channel formats
|
||||
else if(audioFormat.getChannels() == 2 || audioFormat.getChannels() > 8)
|
||||
{
|
||||
if(audioFormat.getSampleRate() == AudioSystem.NOT_SPECIFIED)
|
||||
{
|
||||
discoveredFormats.add(audioFormat);
|
||||
//Modify the format for 8 kHz sample rate, 2 channels, 4 bytes per frame
|
||||
AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
||||
8000, 16, 2, 4,
|
||||
8000, false);
|
||||
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
|
||||
}
|
||||
else if(audioFormat.getSampleRate() == 8000)
|
||||
{
|
||||
discoveredFormats.add(audioFormat);
|
||||
//Modify the format for 8 kHz sample rate, 2 channels, 4 bytes per frame
|
||||
AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
||||
audioFormat.getSampleRate(), 16, 2, 4,
|
||||
audioFormat.getFrameRate(), false);
|
||||
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
|
||||
}
|
||||
}
|
||||
//TODO: remove the 2 channels max restriction once supported
|
||||
// else if(audioFormat.getChannels() > 2 && audioFormat.getChannels() <= 8)
|
||||
// {
|
||||
// if(audioFormat.getSampleRate() == AudioSystem.NOT_SPECIFIED)
|
||||
// {
|
||||
// discoveredFormats.add(audioFormat);
|
||||
// //Modify the format for 8 kHz sample rate
|
||||
// AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
|
||||
// 8000, 16, audioFormat.getChannels(), audioFormat.getFrameSize(),
|
||||
// 8000, false);
|
||||
// devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
|
||||
// }
|
||||
// else if(audioFormat.getSampleRate() == 8000)
|
||||
// {
|
||||
// discoveredFormats.add(audioFormat);
|
||||
// devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, audioFormat));
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Collections.sort(devices);
|
||||
return devices;
|
||||
}
|
||||
|
||||
|
||||
static void main()
|
||||
{
|
||||
List<AudioPlaybackDeviceDescriptor> devices = getAudioPlaybackDevices();
|
||||
for(AudioPlaybackDeviceDescriptor device : devices)
|
||||
{
|
||||
System.out.println(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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,11 +27,11 @@ import io.github.dsheirer.controller.NamingThreadFactory;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.playback.PlayTestAudioRequest;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@@ -51,23 +51,16 @@ import org.slf4j.LoggerFactory;
|
||||
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
||||
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||
private final AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
|
||||
private final Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||
private final List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private final List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
|
||||
private final LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private final ReentrantLock mAudioChannelsLock = new ReentrantLock();
|
||||
private final UserPreferences mUserPreferences;
|
||||
private AudioPlaybackDeviceDescriptor mAudioPlaybackDevice;
|
||||
private AudioOutput mAudioOutput;
|
||||
private ScheduledFuture<?> mProcessingTask;
|
||||
private UserPreferences mUserPreferences;
|
||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
||||
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
|
||||
private List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
|
||||
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledExecutorService mScheduledExecutorService =
|
||||
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
|
||||
private AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
|
||||
private ReentrantLock mAudioOutputLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
@@ -78,20 +71,18 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
mUserPreferences = userPreferences;
|
||||
MyEventBus.getGlobalEventBus().register(this);
|
||||
AudioPlaybackDeviceDescriptor device = mUserPreferences.getPlaybackPreference().getAudioPlaybackDevice();
|
||||
|
||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||
|
||||
if(configuration != null)
|
||||
if(device != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(configuration);
|
||||
setAudioPlaybackDevice(device);
|
||||
}
|
||||
catch(AudioException ae)
|
||||
{
|
||||
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
|
||||
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
|
||||
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
|
||||
mLog.error("Error during setup of audio playback configuration. Attempted to use device [" +
|
||||
device + "]", ae);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -99,13 +90,16 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
mLog.warn("No audio output devices available");
|
||||
}
|
||||
|
||||
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
//Even if we don't have an audio device, setup the queue processor to always process the audio segment queue
|
||||
ScheduledExecutorService scheduledExecutorService =
|
||||
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
|
||||
mProcessingTask = scheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives audio segments from channel audio modules.
|
||||
* @param audioSegment
|
||||
* @param audioSegment to receive and process
|
||||
*/
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
@@ -195,42 +189,43 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
}
|
||||
else if(audioSegment.isLinked())
|
||||
{
|
||||
mAudioOutputLock.lock();
|
||||
|
||||
try
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
mAudioChannelsLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
for(AudioChannel audioOutput: mAudioOutput.getAudioProvider().getAudioChannels())
|
||||
{
|
||||
it.remove();
|
||||
audioOutput.play(audioSegment);
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
{
|
||||
it.remove();
|
||||
audioOutput.play(audioSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
finally
|
||||
{
|
||||
mAudioChannelsLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Sort audio segments by playback priority and assign to empty audio outputs
|
||||
if(!mAudioSegments.isEmpty())
|
||||
if(!mAudioSegments.isEmpty() && mAudioOutput != null)
|
||||
{
|
||||
mAudioSegments.sort(mAudioSegmentPrioritySorter);
|
||||
|
||||
mAudioOutputLock.lock();
|
||||
mAudioChannelsLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
for(AudioChannel audioChannel: mAudioOutput.getAudioProvider().getAudioChannels())
|
||||
{
|
||||
if(audioOutput.isEmpty())
|
||||
if(audioChannel.isEmpty())
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
audioChannel.play(mAudioSegments.removeFirst());
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
return;
|
||||
@@ -240,7 +235,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
mAudioChannelsLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +268,19 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
mAudioSegments.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Receives a request from the global event bus to playback a test audio sequence via the specified audio channel
|
||||
* @param request with test audio and channel number
|
||||
*/
|
||||
@Subscribe
|
||||
public void playTestAudio(PlayTestAudioRequest request)
|
||||
{
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.playTestAudio(request);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive user preference update notifications so that we can detect when the user changes the audio output
|
||||
* device in the user preferences editor.
|
||||
@@ -282,100 +290,112 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
{
|
||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||
AudioPlaybackDeviceDescriptor device = mUserPreferences.getPlaybackPreference().getAudioPlaybackDevice();
|
||||
|
||||
if(configuration != null && !configuration.equals(mMixerChannelConfiguration))
|
||||
if(device != null && !device.equals(getAudioPlaybackDevice()))
|
||||
{
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(configuration);
|
||||
setAudioPlaybackDevice(device);
|
||||
}
|
||||
catch(AudioException ae)
|
||||
{
|
||||
mLog.error("Error changing audio output to [" + configuration + "]", ae);
|
||||
mLog.error("Error changing audio output to [" + device + "]", ae);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures audio playback to use the configuration specified in the entry argument.
|
||||
* Configures audio playback to use the audio device argument.
|
||||
*
|
||||
* @param entry to use in configuring the audio playback setup.
|
||||
* @param audioDevice to use in configuring the audio playback setup.
|
||||
* @throws AudioException if there is an error
|
||||
*/
|
||||
@Override
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
|
||||
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor audioDevice) throws AudioException
|
||||
{
|
||||
if(entry != null)
|
||||
if(audioDevice != null)
|
||||
{
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
||||
mControllerBroadcaster.broadcast(new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED,
|
||||
audioDevice.getMixerInfo().getName()));
|
||||
|
||||
mAudioOutputLock.lock();
|
||||
mAudioChannelsLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
audioOutput.dispose();
|
||||
mAudioOutput.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputs.clear();
|
||||
int channelCount = audioDevice.getAudioFormat().getChannels();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
switch(channelCount)
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||
mAudioOutputs.add(mono);
|
||||
case 1:
|
||||
mAudioOutput = new AudioOutput(audioDevice, new AudioProviderMono(mUserPreferences));
|
||||
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);
|
||||
case 2:
|
||||
mAudioOutput = new AudioOutput(audioDevice, new AudioProviderStereo(mUserPreferences));
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||
// mAudioOutput = new AudioOutput(audioDevice, new AudioProviderMultiChannel(mUserPreferences,
|
||||
// audioDevice.getAudioFormat()));
|
||||
throw new AudioException("Unsupported mixer channel configuration channel count: " + channelCount);
|
||||
}
|
||||
|
||||
//Note: audio output can use an alternate device if the requested device can't be used, so we assign
|
||||
//the descriptor that was actually used by the audio output
|
||||
mAudioPlaybackDevice = mAudioOutput.getAudioPlaybackDeviceDescriptor();
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
mAudioChannelsLock.unlock();
|
||||
}
|
||||
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
||||
mMixerChannelConfiguration = entry;
|
||||
mControllerBroadcaster.broadcast(new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE,
|
||||
mAudioPlaybackDevice.getMixerInfo().getName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio output device. Note: this can be null depending on when it is accessed.
|
||||
* @return audio output or null.
|
||||
*/
|
||||
public AudioOutput getAudioOutput()
|
||||
{
|
||||
return mAudioOutput;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current audio playback mixer channel configuration setting.
|
||||
*/
|
||||
@Override
|
||||
public MixerChannelConfiguration getMixerChannelConfiguration()
|
||||
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice()
|
||||
{
|
||||
return mMixerChannelConfiguration;
|
||||
return mAudioPlaybackDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* List of sorted audio outputs available for the current mixer channel configuration
|
||||
*/
|
||||
@Override
|
||||
public List<AudioOutput> getAudioOutputs()
|
||||
public List<AudioChannel> getAudioChannels()
|
||||
{
|
||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputs);
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
return mAudioOutput.getAudioProvider().getAudioChannels();
|
||||
}
|
||||
|
||||
outputs.sort(Comparator.comparing(AudioOutput::getChannelName));
|
||||
|
||||
return outputs;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an audio event listener to receive audio event notifications.
|
||||
*/
|
||||
@Override
|
||||
public void addControllerListener(Listener<AudioEvent> listener)
|
||||
public void addAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mControllerBroadcaster.addListener(listener);
|
||||
}
|
||||
@@ -384,7 +404,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
* Removes an audio event listener from receiving audio event notifications.
|
||||
*/
|
||||
@Override
|
||||
public void removeControllerListener(Listener<AudioEvent> listener)
|
||||
public void removeAudioEventListener(Listener<AudioEvent> listener)
|
||||
{
|
||||
mControllerBroadcaster.removeListener(listener);
|
||||
}
|
||||
@@ -394,7 +414,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
*/
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
private final AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
@@ -418,7 +438,7 @@ 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>
|
||||
public static class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
|
||||
{
|
||||
@Override
|
||||
public int compare(AudioSegment segment1, AudioSegment segment2)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
|
||||
/**
|
||||
* Abstract audio provider implementation
|
||||
*/
|
||||
public abstract class AudioProvider
|
||||
{
|
||||
/**
|
||||
* Audio format for this provider
|
||||
*/
|
||||
public abstract AudioFormat getAudioFormat();
|
||||
|
||||
/**
|
||||
* Provide a non-null, 160-sample/20-millisecond buffer of audio at 8 kHz
|
||||
* @return audio buffer
|
||||
*/
|
||||
public abstract ByteBuffer getAudio();
|
||||
|
||||
/**
|
||||
* Provides access to the audio channels for this provider
|
||||
* @return list of audio channels
|
||||
*/
|
||||
public abstract List<AudioChannel> getAudioChannels();
|
||||
|
||||
/**
|
||||
* Notifies audio event listener(s) of the event
|
||||
* @param eventType to broadcast
|
||||
*/
|
||||
public void notify(AudioEvent.Type eventType)
|
||||
{
|
||||
for(AudioChannel audioChannel : getAudioChannels())
|
||||
{
|
||||
audioChannel.notify(eventType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the audio channel to prepare for audio output changes or shutdown.
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
for(AudioChannel audioChannel: getAudioChannels())
|
||||
{
|
||||
audioChannel.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
|
||||
/**
|
||||
* Single channel (mono) audio provider wraps a single audio channel and provides converted audio.
|
||||
*/
|
||||
public class AudioProviderMono extends AudioProvider
|
||||
{
|
||||
private static final ByteBuffer SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2);
|
||||
private final AudioChannel mAudioChannel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param userPreferences for monitoring preference changes
|
||||
*/
|
||||
public AudioProviderMono(UserPreferences userPreferences)
|
||||
{
|
||||
mAudioChannel = new AudioChannel(userPreferences, "MONO");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AudioChannel> getAudioChannels()
|
||||
{
|
||||
return List.of(mAudioChannel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioFormat getAudioFormat()
|
||||
{
|
||||
return AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer getAudio()
|
||||
{
|
||||
float[] audio = mAudioChannel.getAudio();
|
||||
|
||||
if(audio == null)
|
||||
{
|
||||
if(!mAudioChannel.hasAudioSegment())
|
||||
{
|
||||
mAudioChannel.clearMetadata();
|
||||
}
|
||||
|
||||
return SILENCE;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
for(int x = 0; x < AudioChannel.SAMPLES_PER_INTERVAL; x++)
|
||||
{
|
||||
shortBuffer.put((short) (audio[x] * Short.MAX_VALUE));
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
|
||||
/**
|
||||
* Multi-Channel audio provider implementation. Provides an interleaved stream of audio from multiple channels.
|
||||
*
|
||||
* Note: this implementation has not been tested for use.
|
||||
*/
|
||||
public class AudioProviderMultiChannel extends AudioProvider
|
||||
{
|
||||
private static ByteBuffer SILENCE;
|
||||
private final List<AudioChannel> mAudioChannels = new ArrayList<>();
|
||||
private final AudioFormat mAudioFormat;
|
||||
|
||||
/**
|
||||
* Constructs a multi-channel audio provider
|
||||
* @param userPreferences for this provider
|
||||
* @param audioFormat for this provider
|
||||
*/
|
||||
public AudioProviderMultiChannel(UserPreferences userPreferences, AudioFormat audioFormat)
|
||||
{
|
||||
SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * audioFormat.getChannels());
|
||||
|
||||
for(int channel = 0; channel < audioFormat.getChannels(); channel++)
|
||||
{
|
||||
mAudioChannels.add(new AudioChannel(userPreferences, String.valueOf(channel)));
|
||||
}
|
||||
|
||||
mAudioFormat = audioFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioFormat getAudioFormat()
|
||||
{
|
||||
return mAudioFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the next buffer of audio.
|
||||
* @return next buffer or null if there is no audio available
|
||||
*/
|
||||
@Override
|
||||
public ByteBuffer getAudio()
|
||||
{
|
||||
float[][] samples = new float[mAudioChannels.size()][];
|
||||
boolean hasAudio = false;
|
||||
|
||||
AudioChannel channel = null;
|
||||
|
||||
for(int x = 0; x < mAudioChannels.size(); x++)
|
||||
{
|
||||
channel = mAudioChannels.get(x);
|
||||
samples[x] = channel.getAudio();
|
||||
|
||||
if(samples[x] == null)
|
||||
{
|
||||
if(!channel.hasAudioSegment())
|
||||
{
|
||||
channel.clearMetadata();
|
||||
}
|
||||
|
||||
samples[x] = new float[AudioChannel.SAMPLES_PER_INTERVAL];
|
||||
}
|
||||
else
|
||||
{
|
||||
hasAudio = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasAudio)
|
||||
{
|
||||
return SILENCE;
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2 *
|
||||
mAudioChannels.size()).order(ByteOrder.LITTLE_ENDIAN);
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
for(int sampleCounter = 0; sampleCounter < AudioChannel.SAMPLES_PER_INTERVAL; sampleCounter++)
|
||||
{
|
||||
for(int channelNumber = 0; channelNumber < mAudioChannels.size(); channelNumber++)
|
||||
{
|
||||
shortBuffer.put((short)(samples[channelNumber][sampleCounter] * Short.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AudioChannel> getAudioChannels()
|
||||
{
|
||||
return Collections.unmodifiableList(mAudioChannels);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
import java.util.List;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
|
||||
/**
|
||||
* Provides an interleaved stream of left and right audio samples from audio channels.
|
||||
*
|
||||
* Note: the getAudio() method is optimized to provide buffers of 160 samples (x2) of audio per interval, equivalent to
|
||||
* 20 milliseconds of audio at 8,000 Hertz sample rate.
|
||||
*/
|
||||
public class AudioProviderStereo extends AudioProvider
|
||||
{
|
||||
private final AudioChannel mAudioChannelLeft;
|
||||
private final AudioChannel mAudioChannelRight;
|
||||
private static final ByteBuffer SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 4);
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param userPreferences for monitoring preference changes
|
||||
*/
|
||||
public AudioProviderStereo(UserPreferences userPreferences)
|
||||
{
|
||||
mAudioChannelLeft = new AudioChannel(userPreferences, "LEFT");
|
||||
mAudioChannelRight = new AudioChannel(userPreferences, "RIGHT");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioFormat getAudioFormat()
|
||||
{
|
||||
return AudioFormats.PCM_SIGNED_8000_HZ_16BITS_STEREO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AudioChannel> getAudioChannels()
|
||||
{
|
||||
return List.of(mAudioChannelLeft, mAudioChannelRight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the next buffer of audio.
|
||||
* @return next buffer or null if there is no audio available
|
||||
*/
|
||||
@Override
|
||||
public ByteBuffer getAudio()
|
||||
{
|
||||
float[] left = mAudioChannelLeft.getAudio();
|
||||
|
||||
if(left == null && !mAudioChannelLeft.hasAudioSegment())
|
||||
{
|
||||
mAudioChannelLeft.clearMetadata();
|
||||
}
|
||||
|
||||
float[] right = mAudioChannelRight.getAudio();
|
||||
|
||||
if(right == null && !mAudioChannelRight.hasAudioSegment())
|
||||
{
|
||||
mAudioChannelRight.clearMetadata();
|
||||
}
|
||||
|
||||
//If we didn't get audio from either channel send silence buffer
|
||||
if(left == null && right == null)
|
||||
{
|
||||
return SILENCE;
|
||||
}
|
||||
else if(left == null)
|
||||
{
|
||||
left = new float[AudioChannel.SAMPLES_PER_INTERVAL];
|
||||
}
|
||||
else if(right == null)
|
||||
{
|
||||
right = new float[AudioChannel.SAMPLES_PER_INTERVAL];
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 4).order(ByteOrder.LITTLE_ENDIAN);
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
for(int x = 0; x < AudioChannel.SAMPLES_PER_INTERVAL; x++)
|
||||
{
|
||||
shortBuffer.put((short)(left[x] * Short.MAX_VALUE));
|
||||
shortBuffer.put((short)(right[x] * Short.MAX_VALUE));
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -1,65 +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.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
/**
|
||||
* Mono Audio output implementation
|
||||
*/
|
||||
public class MonoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 8000;
|
||||
|
||||
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
|
||||
{
|
||||
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO,
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into mono audio frames.
|
||||
*/
|
||||
protected ByteBuffer convert(float[] samples)
|
||||
{
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(samples.length > 0)
|
||||
{
|
||||
/* Little-endian byte buffer */
|
||||
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
for(float sample : samples)
|
||||
{
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +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.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
/**
|
||||
* Stereo audio output implementation.
|
||||
*/
|
||||
public class StereoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 16000;
|
||||
|
||||
public StereoAudioOutput(Mixer mixer, MixerChannel channel, UserPreferences userPreferences)
|
||||
{
|
||||
super(mixer, channel, AudioFormats.PCM_SIGNED_8000_HZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO,
|
||||
BUFFER_SIZE, userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into stereo audio frames with the mixer
|
||||
* channel containing the audio and the other channel containing zero
|
||||
* valued (silent) samples.
|
||||
*/
|
||||
protected ByteBuffer convert(float[] samples)
|
||||
{
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(samples.length > 0)
|
||||
{
|
||||
/* Little-endian byte buffer */
|
||||
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
if(getMixerChannel() == MixerChannel.LEFT)
|
||||
{
|
||||
for(float sample : samples)
|
||||
{
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
shortBuffer.put((short) 0);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for(float sample : samples)
|
||||
{
|
||||
shortBuffer.put((short) 0);
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -306,6 +306,15 @@ public class BinaryMessage extends BitSet
|
||||
}
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return toHexString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary representation of the message
|
||||
* @return message in binary 0/1 format
|
||||
*/
|
||||
public String toBinaryString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
@@ -317,6 +326,10 @@ public class BinaryMessage extends BitSet
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hex string representation of the message
|
||||
* @return message in hex
|
||||
*/
|
||||
public String toHexString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -997,6 +1010,18 @@ public class BinaryMessage extends BitSet
|
||||
return String.format("%0" + width + "X", getInt(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the long field formatted as a hex value using zero prefixes to pad the hex character count to fully
|
||||
* represent the size (width) of the field.
|
||||
* @param field to parse as hex
|
||||
* @return hex value.
|
||||
*/
|
||||
public String getHex(LongField field)
|
||||
{
|
||||
int width = Math.ceilDiv(field.width(), 4);
|
||||
return String.format("%0" + width + "X", getLong(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less
|
||||
* than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in
|
||||
@@ -1315,37 +1340,15 @@ public class BinaryMessage extends BitSet
|
||||
throw new IllegalArgumentException("Message must contain only 0-9 and A-F hexadecimal characters");
|
||||
}
|
||||
|
||||
int length = hex.length() / 2;
|
||||
BinaryMessage message = new BinaryMessage(hex.length() * 4);
|
||||
|
||||
if(length * 2 < hex.length())
|
||||
for(int x = 0; x < hex.length(); x++)
|
||||
{
|
||||
length++;
|
||||
String rawHex = hex.substring(x, x + 1);
|
||||
message.setInt(Integer.parseInt(rawHex, 16), IntField.length4(x * 4));
|
||||
}
|
||||
|
||||
BinaryMessage buffer = new BinaryMessage(length * 8);
|
||||
|
||||
int bufferOffset = 0;
|
||||
for(int x = 0; x < hex.length(); x += 2)
|
||||
{
|
||||
int endIndex = x + 2;
|
||||
if(endIndex > hex.length())
|
||||
{
|
||||
endIndex--;
|
||||
}
|
||||
|
||||
String rawHex = hex.substring(x, endIndex);
|
||||
|
||||
if(rawHex.length() == 1)
|
||||
{
|
||||
rawHex = rawHex + "0";
|
||||
}
|
||||
|
||||
int value = Integer.parseInt(rawHex, 16);
|
||||
buffer.setByte(bufferOffset, (byte)(value & 0xFF));
|
||||
bufferOffset += 8;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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,6 +18,7 @@
|
||||
*/
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.BitSet;
|
||||
|
||||
public class CorrectedBinaryMessage extends BinaryMessage
|
||||
@@ -54,6 +55,45 @@ public class CorrectedBinaryMessage extends BinaryMessage
|
||||
this.xor(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new message loaded from the hexadecimal string
|
||||
* @param hex representing the bits of the message
|
||||
* @return new message
|
||||
*/
|
||||
public static CorrectedBinaryMessage loadHex(String hex)
|
||||
{
|
||||
return new CorrectedBinaryMessage(BinaryMessage.loadHex(hex));
|
||||
}
|
||||
|
||||
/**
|
||||
* Overloads the parent method to return this type.
|
||||
* @param binary message to load
|
||||
* @return loaded message
|
||||
*/
|
||||
public static CorrectedBinaryMessage load(String binary)
|
||||
{
|
||||
return new CorrectedBinaryMessage(BinaryMessage.load(binary));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a binary frame from the byte array with length set to the
|
||||
* number of bits contained in the byte array
|
||||
*/
|
||||
public static CorrectedBinaryMessage from(byte[] data, ByteOrder byteOrder)
|
||||
{
|
||||
if(byteOrder == ByteOrder.LITTLE_ENDIAN)
|
||||
{
|
||||
return new CorrectedBinaryMessage(from(data));
|
||||
}
|
||||
else
|
||||
{
|
||||
CorrectedBinaryMessage frame = new CorrectedBinaryMessage(data.length * 8);
|
||||
BitSet bitSet = BitSet.valueOf(data);
|
||||
frame.xor(bitSet);
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCorrectedBitCount()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2025 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -60,7 +60,27 @@ public record IntField(int start, int end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one four bits of length.
|
||||
* Utility constructor method for a field with two bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length2(int start)
|
||||
{
|
||||
return new IntField(start, (start + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with three bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length3(int start)
|
||||
{
|
||||
return new IntField(start, (start + 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with four bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
@@ -70,7 +90,17 @@ public record IntField(int start, int end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one four bits of length.
|
||||
* Utility constructor method for a field with five bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length5(int start)
|
||||
{
|
||||
return new IntField(start, (start + 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with six bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
@@ -79,6 +109,16 @@ public record IntField(int start, int end)
|
||||
return new IntField(start, (start + 5));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with seven bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length7(int start)
|
||||
{
|
||||
return new IntField(start, (start + 6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one octet of length.
|
||||
* @param start index (inclusive)
|
||||
@@ -89,6 +129,36 @@ public record IntField(int start, int end)
|
||||
return new IntField(start, (start + 7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with nine bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length9(int start)
|
||||
{
|
||||
return new IntField(start, (start + 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with ten bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length10(int start)
|
||||
{
|
||||
return new IntField(start, (start + 9));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with eleven bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length11(int start)
|
||||
{
|
||||
return new IntField(start, (start + 10));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 12 bits of length.
|
||||
* @param start index (inclusive)
|
||||
@@ -99,6 +169,26 @@ public record IntField(int start, int end)
|
||||
return new IntField(start, (start + 11));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 14 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length14(int start)
|
||||
{
|
||||
return new IntField(start, (start + 13));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 15 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length15(int start)
|
||||
{
|
||||
return new IntField(start, (start + 14));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -39,6 +39,15 @@ public record LongField(int start, int end)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the field in bit positions
|
||||
* @return width
|
||||
*/
|
||||
public int width()
|
||||
{
|
||||
return end() - start() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method.
|
||||
* @param start index (inclusive)
|
||||
@@ -101,7 +110,17 @@ public record LongField(int start, int end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* Utility constructor method for a field with 23 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length23(int start)
|
||||
{
|
||||
return new LongField(start, (start + 22));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with three octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
@@ -111,7 +130,7 @@ public record LongField(int start, int end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with three octets of length.
|
||||
* Utility constructor method for a field with four octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
@@ -119,4 +138,24 @@ public record LongField(int start, int end)
|
||||
{
|
||||
return new LongField(start, (start + 31));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with seven octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length56(int start)
|
||||
{
|
||||
return new LongField(start, (start + 55));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with eight octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length64(int start)
|
||||
{
|
||||
return new LongField(start, (start + 63));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -98,6 +98,33 @@ public class FloatCircularBuffer
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all sample values in reverse order.
|
||||
*/
|
||||
public float[] getAllReversed()
|
||||
{
|
||||
int valuePointer = 0;
|
||||
float[] values = new float[mBuffer.length];
|
||||
int bufferPointer = mBufferPointer - 1;
|
||||
|
||||
if(bufferPointer < 0)
|
||||
{
|
||||
bufferPointer = mBuffer.length - 1;
|
||||
}
|
||||
|
||||
while(valuePointer < values.length)
|
||||
{
|
||||
values[valuePointer++] = mBuffer[bufferPointer--];
|
||||
|
||||
if(bufferPointer < 0)
|
||||
{
|
||||
bufferPointer = mBuffer.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the new value into the buffer, overwriting the oldest value
|
||||
* @param newValue to add to the buffer
|
||||
|
||||
@@ -100,8 +100,10 @@ public class FloatNativeBuffer extends AbstractNativeBuffer
|
||||
throw new IllegalStateException("No more samples");
|
||||
}
|
||||
|
||||
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
|
||||
mBufferPointer += BUFFER_SIZE * 2;
|
||||
//The final chunk holds the remaining samples, so that it isn't padded with zero valued samples
|
||||
int end = Math.min(mBufferPointer + BUFFER_SIZE * 2, mInterleavedComplexSamples.length);
|
||||
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, end);
|
||||
mBufferPointer = end;
|
||||
|
||||
return SampleUtils.deinterleave(chunk, getTimestamp());
|
||||
}
|
||||
@@ -125,8 +127,10 @@ public class FloatNativeBuffer extends AbstractNativeBuffer
|
||||
throw new IllegalStateException("No more samples");
|
||||
}
|
||||
|
||||
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
|
||||
mBufferPointer += BUFFER_SIZE * 2;
|
||||
//The final chunk holds the remaining samples, so that it isn't padded with zero valued samples
|
||||
int end = Math.min(mBufferPointer + BUFFER_SIZE * 2, mInterleavedComplexSamples.length);
|
||||
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, end);
|
||||
mBufferPointer = end;
|
||||
|
||||
return new InterleavedComplexSamples(chunk, getTimestamp());
|
||||
}
|
||||
|
||||
@@ -116,7 +116,8 @@ public class NativeSampleDelayBuffer implements Listener<INativeBuffer>
|
||||
actionRequest = mActionQueue.poll();
|
||||
}
|
||||
|
||||
mBroadcaster.receive(samples);
|
||||
//The delay queue stores the unconverted buffer since it's more compact than the converted samples
|
||||
mBroadcaster.receive(mBroadcaster.getListenerCount() > 1 ? new SharedNativeBuffer(samples) : samples);
|
||||
|
||||
//Store the new buffer in the delay queue and increment the pointer
|
||||
mDelayBuffer[mDelayBufferPointer++] = samples;
|
||||
|
||||
114
src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java
Normal file
114
src/main/java/io/github/dsheirer/buffer/SharedNativeBuffer.java
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.buffer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Native buffer wrapper that lets multiple channels share one conversion of the native samples. Each channel's
|
||||
* iterator returns the same complex samples instances, on the channel's own thread, so consumers must not modify the
|
||||
* samples. The converted samples are released when the channels release this wrapper.
|
||||
*/
|
||||
public class SharedNativeBuffer implements INativeBuffer
|
||||
{
|
||||
private final INativeBuffer mNativeBuffer;
|
||||
private final List<ComplexSamples> mConverted = new ArrayList<>();
|
||||
private Iterator<ComplexSamples> mConverter;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param nativeBuffer to convert
|
||||
*/
|
||||
public SharedNativeBuffer(INativeBuffer nativeBuffer)
|
||||
{
|
||||
mNativeBuffer = nativeBuffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the converted samples include the index, converting the next samples as needed.
|
||||
*/
|
||||
private synchronized boolean hasConverted(int index)
|
||||
{
|
||||
if(mConverter == null)
|
||||
{
|
||||
mConverter = mNativeBuffer.iterator();
|
||||
}
|
||||
|
||||
if(index == mConverted.size() && mConverter.hasNext())
|
||||
{
|
||||
mConverted.add(mConverter.next());
|
||||
}
|
||||
|
||||
return index < mConverted.size();
|
||||
}
|
||||
|
||||
private synchronized ComplexSamples getConverted(int index)
|
||||
{
|
||||
return mConverted.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ComplexSamples> iterator()
|
||||
{
|
||||
return new Iterator<>()
|
||||
{
|
||||
private int mIndex = 0;
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
return hasConverted(mIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComplexSamples next()
|
||||
{
|
||||
if(!hasNext())
|
||||
{
|
||||
throw new NoSuchElementException("No more samples");
|
||||
}
|
||||
|
||||
return getConverted(mIndex++);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<InterleavedComplexSamples> iteratorInterleaved()
|
||||
{
|
||||
return mNativeBuffer.iteratorInterleaved();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sampleCount()
|
||||
{
|
||||
return mNativeBuffer.sampleCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mNativeBuffer.getTimestamp();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy.hf;
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.buffer.AbstractNativeBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy.hf;
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
|
||||
import io.github.dsheirer.buffer.DcCorrectionManager;
|
||||
@@ -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
|
||||
@@ -17,18 +17,18 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Interface to convert airspy samples from a byte buffer to an array of shorts.
|
||||
* Interface to convert samples from a byte buffer to an array of shorts.
|
||||
*/
|
||||
public interface IAirspySampleConverter
|
||||
public interface ISampleConverter
|
||||
{
|
||||
/**
|
||||
* Converts the airspy byte samples contained in the byte buffer to their short-valued representation
|
||||
* @param buffer of airspy samples
|
||||
* Converts the byte samples contained in the byte buffer to their short-valued representation
|
||||
* @param buffer of samples
|
||||
* @return converted samples
|
||||
*/
|
||||
short[] convert(ByteBuffer buffer);
|
||||
@@ -17,17 +17,17 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Base complex samples iterator for raw Airspy sample buffers. Incorporates a Hilbert transform filter
|
||||
* Base complex samples iterator for raw sample buffers. Incorporates a Hilbert transform filter
|
||||
* for converting the real sample array to complex samples.
|
||||
* @param <T> either ComplexSamples or InterleavedComplexSamples
|
||||
*/
|
||||
public abstract class AirspyBufferIterator<T> implements Iterator<T>
|
||||
public abstract class SampleBufferIterator<T> implements Iterator<T>
|
||||
{
|
||||
protected static final float SCALE_SIGNED_12_BIT_TO_FLOAT = 1.0f / 2048.0f;
|
||||
protected static final int FRAGMENT_SIZE = 4096;
|
||||
@@ -47,14 +47,14 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param samples from the airspy.
|
||||
* @param samples from the tuner.
|
||||
* @param residualI samples from previous buffer
|
||||
* @param residualQ samples from previous buffer
|
||||
* @param averageDc as measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
|
||||
public SampleBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
|
||||
float samplesPerMillisecond)
|
||||
{
|
||||
if(residualI.length != I_OVERLAP || residualQ.length != Q_OVERLAP)
|
||||
@@ -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
|
||||
@@ -17,27 +17,27 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Scalar implementation for non-packed Airspy native buffers
|
||||
* Scalar implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamples>
|
||||
public class SampleBufferIteratorScalar extends SampleBufferIterator<ComplexSamples>
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
|
||||
public SampleBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
@@ -26,23 +26,23 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<ComplexSamples>
|
||||
public class SampleBufferIteratorVector128Bits extends SampleBufferIterator<ComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
@@ -26,23 +26,23 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<ComplexSamples>
|
||||
public class SampleBufferIteratorVector256Bits extends SampleBufferIterator<ComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
@@ -26,9 +26,9 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<ComplexSamples>
|
||||
public class SampleBufferIteratorVector512Bits extends SampleBufferIterator<ComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
|
||||
private float[] mFilterPart2;
|
||||
@@ -36,14 +36,14 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
@@ -26,23 +26,23 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<ComplexSamples>
|
||||
public class SampleBufferIteratorVector64Bits extends SampleBufferIterator<ComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,26 +17,26 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
|
||||
/**
|
||||
* Scalar implementation for non-packed Airspy native buffers
|
||||
* Scalar implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<InterleavedComplexSamples>
|
||||
public class SampleInterleavedBufferIteratorScalar extends SampleBufferIterator<InterleavedComplexSamples>
|
||||
{
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
@@ -25,23 +25,23 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIterator<InterleavedComplexSamples>
|
||||
public class SampleInterleavedBufferIteratorVector128Bits extends SampleBufferIterator<InterleavedComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
@@ -25,23 +25,23 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIterator<InterleavedComplexSamples>
|
||||
public class SampleInterleavedBufferIteratorVector256Bits extends SampleBufferIterator<InterleavedComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
@@ -25,9 +25,9 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIterator<InterleavedComplexSamples>
|
||||
public class SampleInterleavedBufferIteratorVector512Bits extends SampleBufferIterator<InterleavedComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
|
||||
private float[] mFilterPart2;
|
||||
@@ -35,14 +35,14 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
@@ -25,23 +25,23 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
* Vector SIMD implementation for non-packed native buffers
|
||||
*/
|
||||
public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIterator<InterleavedComplexSamples>
|
||||
public class SampleInterleavedBufferIteratorVector64Bits extends SampleBufferIterator<InterleavedComplexSamples>
|
||||
{
|
||||
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param samples from the airspy, either packed or unpacked.
|
||||
* @param samples from the device, either packed or unpacked.
|
||||
* @param residualI samples from last buffer
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.buffer.AbstractNativeBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -26,20 +26,20 @@ import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Native buffer scalar implementation for Airspy non-packed samples.
|
||||
* Native buffer scalar implementation for non-packed samples.
|
||||
*/
|
||||
public class AirspyNativeBuffer extends AbstractNativeBuffer
|
||||
public class SampleNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
private short[] mSamples;
|
||||
private short[] mResidualI;
|
||||
private short[] mResidualQ;
|
||||
private float mAverageDc;
|
||||
private Implementation mInterleavedImplementation;
|
||||
private Implementation mNonInterleavedImplementation;
|
||||
private final short[] mSamples;
|
||||
private final short[] mResidualI;
|
||||
private final short[] mResidualQ;
|
||||
private final float mAverageDc;
|
||||
private final Implementation mInterleavedImplementation;
|
||||
private final Implementation mNonInterleavedImplementation;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param samples (non-packed) from the airspy device
|
||||
* @param samples (non-packed) from the device
|
||||
* @param residualI samples from previous buffer
|
||||
* @param residualQ samples from previous buffer
|
||||
* @param averageDc measured
|
||||
@@ -48,17 +48,17 @@ public class AirspyNativeBuffer extends AbstractNativeBuffer
|
||||
* @param nonInterleavedImplementation optimal, scalar vs vector SIMD
|
||||
* @param samplesPerMillisecond used to calculate sub-buffer fragment timestamp offsets from the start of this buffer.
|
||||
*/
|
||||
public AirspyNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
public SampleNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, Implementation interleavedImplementation,
|
||||
Implementation nonInterleavedImplementation, float samplesPerMillisecond)
|
||||
{
|
||||
super(timestamp, samplesPerMillisecond);
|
||||
|
||||
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
|
||||
if(samples.length % AirspyBufferIterator.FRAGMENT_SIZE != 0)
|
||||
if(samples.length % SampleBufferIterator.FRAGMENT_SIZE != 0)
|
||||
{
|
||||
throw new IllegalArgumentException("Samples short[] length [" + samples.length +
|
||||
"] must be an even multiple of " + AirspyBufferIterator.FRAGMENT_SIZE);
|
||||
"] must be an even multiple of " + SampleBufferIterator.FRAGMENT_SIZE);
|
||||
}
|
||||
|
||||
mSamples = samples;
|
||||
@@ -78,13 +78,13 @@ public class AirspyNativeBuffer extends AbstractNativeBuffer
|
||||
@Override
|
||||
public Iterator<ComplexSamples> iterator()
|
||||
{
|
||||
return switch(mInterleavedImplementation)
|
||||
return switch(mNonInterleavedImplementation)
|
||||
{
|
||||
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_512 -> new SampleBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_256-> new SampleBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_128-> new SampleBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_64 ->new SampleBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
default -> new SampleBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,11 +93,11 @@ public class AirspyNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
return switch(mInterleavedImplementation)
|
||||
{
|
||||
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_512 -> new SampleInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_256-> new SampleInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_128-> new SampleInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_64 ->new SampleInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
default -> new SampleInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
|
||||
import io.github.dsheirer.buffer.INativeBuffer;
|
||||
@@ -30,26 +30,26 @@ import java.util.Arrays;
|
||||
/**
|
||||
* Implements a factory for creating SignedByteNativeBuffer instances
|
||||
*/
|
||||
public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
public class SampleNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
{
|
||||
private boolean mSamplePacking = false;
|
||||
private short[] mResidualI = new short[AirspyBufferIterator.I_OVERLAP];
|
||||
private short[] mResidualQ = new short[AirspyBufferIterator.Q_OVERLAP];
|
||||
private IAirspySampleConverter mConverter;
|
||||
private short[] mResidualI = new short[SampleBufferIterator.I_OVERLAP];
|
||||
private short[] mResidualQ = new short[SampleBufferIterator.Q_OVERLAP];
|
||||
private ISampleConverter mConverter;
|
||||
private Implementation mInterleavedIteratorImplementation;
|
||||
private Implementation mNonInterleavedIteratorImplementation;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public AirspyNativeBufferFactory()
|
||||
public SampleNativeBufferFactory()
|
||||
{
|
||||
updateConverter();
|
||||
|
||||
mInterleavedIteratorImplementation = CalibrationManager.getInstance()
|
||||
.getImplementation(CalibrationType.AIRSPY_UNPACKED_INTERLEAVED_ITERATOR);
|
||||
.getImplementation(CalibrationType.SAMPLE_UNPACKED_INTERLEAVED_ITERATOR);
|
||||
mNonInterleavedIteratorImplementation = CalibrationManager.getInstance()
|
||||
.getImplementation(CalibrationType.AIRSPY_UNPACKED_ITERATOR);
|
||||
.getImplementation(CalibrationType.SAMPLE_UNPACKED_ITERATOR);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,7 +78,7 @@ public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
}
|
||||
else
|
||||
{
|
||||
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.AIRSPY_SAMPLE_CONVERTER);
|
||||
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.SAMPLE_PACKED_CONVERTER);
|
||||
|
||||
if(implementation == Implementation.VECTOR_SIMD_PREFERRED)
|
||||
{
|
||||
@@ -102,7 +102,7 @@ public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
{
|
||||
short[] samples = mConverter.convert(buffer);
|
||||
|
||||
INativeBuffer nativeBuffer = new AirspyNativeBuffer(samples,
|
||||
INativeBuffer nativeBuffer = new SampleNativeBuffer(samples,
|
||||
Arrays.copyOf(mResidualI, mResidualI.length),
|
||||
Arrays.copyOf(mResidualQ, mResidualQ.length), mConverter.getAverageDc(), timestamp,
|
||||
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation, getSamplesPerMillisecond());
|
||||
@@ -118,16 +118,16 @@ public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
*/
|
||||
private void extractResidual(short[] samples)
|
||||
{
|
||||
int offset = samples.length - (AirspyBufferIterator.I_OVERLAP * 2);
|
||||
int offset = samples.length - (SampleBufferIterator.I_OVERLAP * 2);
|
||||
|
||||
for(int i = 0; i < AirspyBufferIterator.I_OVERLAP; i++)
|
||||
for(int i = 0; i < SampleBufferIterator.I_OVERLAP; i++)
|
||||
{
|
||||
mResidualI[i] = samples[offset + (2 * i)];
|
||||
}
|
||||
|
||||
offset = samples.length - (AirspyBufferIterator.Q_OVERLAP * 2) + 1;
|
||||
offset = samples.length - (SampleBufferIterator.Q_OVERLAP * 2) + 1;
|
||||
|
||||
for(int q = 0; q < AirspyBufferIterator.Q_OVERLAP; q++)
|
||||
for(int q = 0; q < SampleBufferIterator.Q_OVERLAP; q++)
|
||||
{
|
||||
mResidualQ[q] = samples[offset + (2 * q)];
|
||||
}
|
||||
@@ -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
|
||||
@@ -17,15 +17,15 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.buffer.DcCorrectionManager;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Scalar implementation of airspy sample converter for un-packed samples.
|
||||
* Scalar implementation of sample converter for un-packed samples.
|
||||
*/
|
||||
public class ScalarPackedSampleConverter implements IAirspySampleConverter
|
||||
public class ScalarPackedSampleConverter implements ISampleConverter
|
||||
{
|
||||
/**
|
||||
* Manages DC calculations and processing interval
|
||||
@@ -64,7 +64,7 @@ public class ScalarPackedSampleConverter implements IAirspySampleConverter
|
||||
}
|
||||
|
||||
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
|
||||
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
|
||||
averageDcNow *= SampleBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
|
||||
mDcCalculationManager.adjust(averageDcNow);
|
||||
}
|
||||
else
|
||||
@@ -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
|
||||
@@ -17,15 +17,15 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.buffer.DcCorrectionManager;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Scalar implementation of airspy sample converter for un-packed samples.
|
||||
* Scalar implementation of sample converter for un-packed samples.
|
||||
*/
|
||||
public class ScalarUnpackedSampleConverter implements IAirspySampleConverter
|
||||
public class ScalarUnpackedSampleConverter implements ISampleConverter
|
||||
{
|
||||
/**
|
||||
* Manages DC calculations and processing interval
|
||||
@@ -58,7 +58,7 @@ public class ScalarUnpackedSampleConverter implements IAirspySampleConverter
|
||||
}
|
||||
|
||||
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
|
||||
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
|
||||
averageDcNow *= SampleBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
|
||||
mDcCalculationManager.adjust(averageDcNow);
|
||||
}
|
||||
else
|
||||
@@ -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
|
||||
@@ -17,7 +17,7 @@
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
package io.github.dsheirer.buffer.sample;
|
||||
|
||||
import io.github.dsheirer.buffer.DcCorrectionManager;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -26,9 +26,9 @@ import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
/**
|
||||
* Scalar implementation of airspy sample converter for un-packed samples.
|
||||
* Scalar implementation of sample converter for un-packed samples.
|
||||
*/
|
||||
public class VectorUnpackedSampleConverter implements IAirspySampleConverter
|
||||
public class VectorUnpackedSampleConverter implements ISampleConverter
|
||||
{
|
||||
private static final VectorSpecies<Short> VECTOR_SPECIES = ShortVector.SPECIES_PREFERRED;
|
||||
|
||||
@@ -88,7 +88,7 @@ public class VectorUnpackedSampleConverter implements IAirspySampleConverter
|
||||
}
|
||||
|
||||
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
|
||||
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
|
||||
averageDcNow *= SampleBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
|
||||
mDcCalculationManager.adjust(averageDcNow);
|
||||
}
|
||||
else
|
||||
@@ -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.channel;
|
||||
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
||||
|
||||
/**
|
||||
* Frequency band channel descriptor
|
||||
*/
|
||||
public interface IBandChannelDescriptor extends IChannelDescriptor
|
||||
{
|
||||
|
||||
/**
|
||||
* Frequency band identifiers (0 - 15)
|
||||
*/
|
||||
int[] getFrequencyBandIdentifiers();
|
||||
|
||||
/**
|
||||
* Assigns the frequency band message to the channel to use in calculating the up/downlink frequencies
|
||||
* @param bandIdentifier
|
||||
*/
|
||||
void setFrequencyBand(IFrequencyBand bandIdentifier);
|
||||
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 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
|
||||
@@ -15,41 +14,29 @@
|
||||
*
|
||||
* 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.channel;
|
||||
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.IFrequencyBand;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Channel descriptor
|
||||
*/
|
||||
public interface IChannelDescriptor
|
||||
{
|
||||
/**
|
||||
* Downlink frequency for the channel
|
||||
*
|
||||
* @return frequency in hertz
|
||||
*/
|
||||
long getDownlinkFrequency();
|
||||
|
||||
/**
|
||||
* Uplink frequency for the channel
|
||||
*
|
||||
* @return frequency in hertz
|
||||
*/
|
||||
long getUplinkFrequency();
|
||||
|
||||
/**
|
||||
* Frequency band identifiers (0 - 15)
|
||||
*/
|
||||
int[] getFrequencyBandIdentifiers();
|
||||
|
||||
/**
|
||||
* Assigns the frequency band message to the channel to use in calculating the up/downlink frequencies
|
||||
* @param bandIdentifier
|
||||
*/
|
||||
void setFrequencyBand(IFrequencyBand bandIdentifier);
|
||||
|
||||
/**
|
||||
* Indicates if the timeslot count is greater than 1;
|
||||
*/
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.controller.channel.ChannelModel;
|
||||
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.gui.playlist.channel.ViewChannelRequest;
|
||||
import io.github.dsheirer.gui.theme.ThemeManager;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
@@ -246,7 +247,8 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
label.setText(Joiner.on(", ").skipNulls().join(aliases));
|
||||
label.setIcon(mIconModel.getIcon(aliases.get(0).getIconName(), IconModel.DEFAULT_ICON_SIZE));
|
||||
label.setForeground(aliases.get(0).getDisplayColor());
|
||||
label.setForeground(ThemeManager.getInstance().getReadableColor(aliases.get(0).getDisplayColor(),
|
||||
label.getBackground()));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -28,7 +28,6 @@ import io.github.dsheirer.module.decode.event.MessageActivityPanel;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import java.awt.Color;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
@@ -98,7 +97,6 @@ public class NowPlayingPanel extends JPanel
|
||||
mTabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
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 -> mChannelSpectrumSquelchPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||
.indexOfComponent(mChannelSpectrumSquelchPanel)));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -523,19 +523,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, timeslot));
|
||||
}
|
||||
|
||||
break;
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||
//Rebroadcast frequency error measurements to external listener if we're currently
|
||||
//in an active (ie sync locked) state.
|
||||
for(int timeslot: mTimeslots)
|
||||
{
|
||||
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState()))
|
||||
{
|
||||
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
|
||||
getChannel().getChannelType().name()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -26,6 +26,7 @@ import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.controller.channel.ChannelConfigurationChangeNotification;
|
||||
import io.github.dsheirer.controller.channel.ChannelEvent;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
@@ -46,6 +47,7 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
@@ -217,28 +219,45 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
*/
|
||||
private void createConfigurationIdentifiers(Channel channel)
|
||||
{
|
||||
mIdentifierCollection.update(DecoderTypeConfigurationIdentifier.create(channel.getDecodeConfiguration().getDecoderType()));
|
||||
List<Identifier<?>> identifiers = createChannelIdentifiers(channel);
|
||||
|
||||
for(Identifier<?> identifier : identifiers)
|
||||
{
|
||||
mIdentifierCollection.update(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates channel metadata identifiers.
|
||||
* @param channel to process
|
||||
* @return list of identifiers
|
||||
*/
|
||||
public static List<Identifier<?>> createChannelIdentifiers(Channel channel)
|
||||
{
|
||||
List<Identifier<?>> identifiers = new ArrayList<>();
|
||||
|
||||
identifiers.add(DecoderTypeConfigurationIdentifier.create(channel.getDecodeConfiguration().getDecoderType()));
|
||||
|
||||
if(channel.hasSystem())
|
||||
{
|
||||
mIdentifierCollection.update(SystemConfigurationIdentifier.create(channel.getSystem()));
|
||||
identifiers.add(SystemConfigurationIdentifier.create(channel.getSystem()));
|
||||
}
|
||||
if(channel.hasSite())
|
||||
{
|
||||
mIdentifierCollection.update(SiteConfigurationIdentifier.create(channel.getSite()));
|
||||
identifiers.add(SiteConfigurationIdentifier.create(channel.getSite()));
|
||||
}
|
||||
if(channel.getName() != null && !channel.getName().isEmpty())
|
||||
{
|
||||
mIdentifierCollection.update(ChannelNameConfigurationIdentifier.create(channel.getName()));
|
||||
identifiers.add(ChannelNameConfigurationIdentifier.create(channel.getName()));
|
||||
}
|
||||
if(channel.getAliasListName() != null && !channel.getAliasListName().isEmpty())
|
||||
{
|
||||
mIdentifierCollection.update(AliasListConfigurationIdentifier.create(channel.getAliasListName()));
|
||||
identifiers.add(AliasListConfigurationIdentifier.create(channel.getAliasListName()));
|
||||
}
|
||||
if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
long frequency = ((SourceConfigTuner)channel.getSourceConfiguration()).getFrequency();
|
||||
mIdentifierCollection.update(FrequencyConfigurationIdentifier.create(frequency));
|
||||
identifiers.add(FrequencyConfigurationIdentifier.create(frequency));
|
||||
}
|
||||
else if(channel.getSourceConfiguration().getSourceType() == SourceType.TUNER_MULTIPLE_FREQUENCIES)
|
||||
{
|
||||
@@ -246,9 +265,11 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
|
||||
if(frequencies.size() > 0)
|
||||
{
|
||||
mIdentifierCollection.update(FrequencyConfigurationIdentifier.create(frequencies.get(0)));
|
||||
identifiers.add(FrequencyConfigurationIdentifier.create(frequencies.get(0)));
|
||||
}
|
||||
}
|
||||
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,15 +413,6 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
mIdentifierUpdateNotificationProxy.receive(new IdentifierUpdateNotification(
|
||||
FrequencyConfigurationIdentifier.create(frequency), IdentifierUpdateNotification.Operation.SILENT_ADD, 0));
|
||||
break;
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||
//Rebroadcast frequency error measurements to external listener if we're currently
|
||||
//in an active (ie sync locked) state.
|
||||
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState()))
|
||||
{
|
||||
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
|
||||
getChannel().getChannelType().name()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,9 +433,8 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
mSquelchController.setSquelchLock(true);
|
||||
break;
|
||||
case REQUEST_CHANGE_CALL_TIMEOUT:
|
||||
if(event instanceof ChangeChannelTimeoutEvent)
|
||||
if(event instanceof ChangeChannelTimeoutEvent timeout)
|
||||
{
|
||||
ChangeChannelTimeoutEvent timeout = (ChangeChannelTimeoutEvent)event;
|
||||
mStateMachine.setFadeTimeoutBufferMilliseconds(timeout.getCallTimeoutMilliseconds());
|
||||
}
|
||||
case CONTINUATION:
|
||||
@@ -471,7 +482,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
* Proxy between the internal identifier collection and the external update notification listener. This proxy
|
||||
* enables access to internal components to broadcast silent identifier update notifications externally.
|
||||
*/
|
||||
public class IdentifierUpdateNotificationProxy implements Listener<IdentifierUpdateNotification>
|
||||
public static class IdentifierUpdateNotificationProxy implements Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
private Listener<IdentifierUpdateNotification> mIdentifierUpdateNotificationListener;
|
||||
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
package io.github.dsheirer.channel.state;
|
||||
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.channel.IBandChannelDescriptor;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
@@ -89,9 +89,9 @@ public abstract class TimeslotDecoderState extends DecoderState
|
||||
{
|
||||
setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifier).getValue());
|
||||
}
|
||||
else if(identifier instanceof IChannelDescriptor)
|
||||
else if(identifier instanceof IBandChannelDescriptor)
|
||||
{
|
||||
setCurrentChannel((IChannelDescriptor)identifier);
|
||||
setCurrentChannel((IBandChannelDescriptor)identifier);
|
||||
}
|
||||
else if(identifier instanceof FrequencyConfigurationIdentifier fci)
|
||||
{
|
||||
|
||||
@@ -32,10 +32,9 @@ import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.source.tuner.ui.TunerViewPanel;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dimension;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import io.github.dsheirer.gui.theme.ThemedFontIcon;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -100,12 +99,11 @@ public class ControllerPanel extends JPanel
|
||||
}
|
||||
};
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
mTabbedPane.addTab("Now Playing", mNowPlayingPanel);
|
||||
mTabbedPane.addTab("Map", mMapPanel);
|
||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||
|
||||
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
|
||||
Icon playIcon = new ThemedFontIcon(FontAwesome.PLAY_CIRCLE_O, 20);
|
||||
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
|
||||
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
||||
|
||||
|
||||
@@ -418,6 +418,11 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
throw new ChannelException("No Tuner Available");
|
||||
}
|
||||
|
||||
if(source instanceof TunerChannelSource tunerChannelSource)
|
||||
{
|
||||
tunerChannelSource.setStartTimestamp(request.getStartTimestamp());
|
||||
}
|
||||
|
||||
ProcessingChain processingChain = new ProcessingChain(channel, mAliasModel);
|
||||
|
||||
//Certain decoders aggregate the decode events in the parent channel that also includes any events produced
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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,13 +19,13 @@
|
||||
|
||||
package io.github.dsheirer.controller.channel.event;
|
||||
|
||||
import io.github.dsheirer.channel.IBandChannelDescriptor;
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.controller.channel.Channel;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.module.ModuleEventBusMessage;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventHistory;
|
||||
import io.github.dsheirer.module.decode.traffic.TrafficChannelManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -42,6 +42,7 @@ public class ChannelStartProcessingRequest extends ModuleEventBusMessage
|
||||
private DecodeEventHistory mParentDecodeEventHistory;
|
||||
private DecodeEventHistory mChildDecodeEventHistory;
|
||||
private boolean mPersistentAttempt;
|
||||
private long mStartTimestamp;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -66,7 +67,7 @@ public class ChannelStartProcessingRequest extends ModuleEventBusMessage
|
||||
* @param channelDescriptor that identifies which channel in a multi-channel config to start
|
||||
* @param identifierCollection to use for the new channel
|
||||
*/
|
||||
public ChannelStartProcessingRequest(Channel channel, IChannelDescriptor channelDescriptor,
|
||||
public ChannelStartProcessingRequest(Channel channel, IBandChannelDescriptor channelDescriptor,
|
||||
IdentifierCollection identifierCollection)
|
||||
{
|
||||
this(channel, channelDescriptor, identifierCollection, null);
|
||||
@@ -109,6 +110,25 @@ public class ChannelStartProcessingRequest extends ModuleEventBusMessage
|
||||
return mPersistentAttempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the timestamp of the channel grant so that a tuner channel source with a sample delay buffer can preload the
|
||||
* channel with the samples that occurred after the grant, while the grant was being decoded and processed.
|
||||
* @param timestamp of the channel grant in milliseconds since epoch.
|
||||
*/
|
||||
public void setStartTimestamp(long timestamp)
|
||||
{
|
||||
mStartTimestamp = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp for the oldest samples requested for the channel.
|
||||
* @return timestamp in milliseconds since epoch, or 0 if the channel should start with current samples.
|
||||
*/
|
||||
public long getStartTimestamp()
|
||||
{
|
||||
return mStartTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel to start processing
|
||||
* @return channel to start
|
||||
|
||||
375
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMEqualizer.java
Normal file
375
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMEqualizer.java
Normal file
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2026 Dennis Sheirer
|
||||
*
|
||||
* This 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.c4fm;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
import org.apache.commons.math3.stat.descriptive.moment.Variance;
|
||||
|
||||
/**
|
||||
* LMS adaptive equalizer for real-valued C4FM symbols. Trains taps on externally triggered sync detection combined
|
||||
* with dynamic tap updates for a fixed symbol count and then freezes the taps until the error variance between sync
|
||||
* detections exceeds a fixed threshold, whereby a tap retraining is enqueued for the next sync detection.
|
||||
*/
|
||||
public class C4FMEqualizer
|
||||
{
|
||||
private static final DecimalFormat DF = new DecimalFormat("0.0000");
|
||||
//Maximum possible error is PI/4 = 0.78, but we clip it to less than half as the allowable error for tap updates
|
||||
private static final float MAX_SYMBOL_ERROR = 0.3f;
|
||||
private static final double TRAINING_IMPROVEMENT_GOAL_PER_ITERATION = 0.0005f;
|
||||
private static final double TRAINING_CONVERGENCE_THRESHOLD = 0.16f;
|
||||
private static final double TRAINING_SKIP_VARIANCE_THRESHOLD = 0.05;
|
||||
private static final int TRAINING_ITERATIONS_MAX = 30;
|
||||
private final Variance mVariance = new Variance();
|
||||
private final float[] mBuffer;
|
||||
private final float[] mReversedSyncSymbols;
|
||||
private final float[] mTaps;
|
||||
private final int mCenterTap;
|
||||
private final int mDelaySymbolCount;
|
||||
private final int mTrainingPeriodSymbolCount;
|
||||
private final int mTrainingSkipCountThreshold;
|
||||
private boolean mEnabled = false;
|
||||
private boolean mSyncDetected = false;
|
||||
private int mSymbolsUntilTraining = 0;
|
||||
private int mTrainingSymbolsRemaining = 0;
|
||||
private int mSkippedTrainingCount = 0;
|
||||
|
||||
/**
|
||||
* Constructs an instance with an equalizer length derived from the sync pattern length.
|
||||
*
|
||||
* @param syncSymbols sync pattern that will be used to train the equalizer on sync detections
|
||||
* @param trainingPeriodSymbolCount following sync detection to continue training.
|
||||
* @param maxNoTrainSyncCount maximum number of consecutive sync detections and skipped trainings that can occur without retraining.
|
||||
*/
|
||||
public C4FMEqualizer(float[] syncSymbols, int trainingPeriodSymbolCount, int maxNoTrainSyncCount)
|
||||
{
|
||||
this(syncSymbols, getDefaultSymbolLength(syncSymbols.length), trainingPeriodSymbolCount, maxNoTrainSyncCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param syncSymbols sync pattern that will be used to train the equalizer on sync detections
|
||||
* @param equalizerSymbolLength equalizer span in symbols. Even values are reduced by one so that the center tap
|
||||
* has equal delay on either side. The tap count is twice this value since this is a fractional (T/2) equalizer.
|
||||
* @param trainingPeriodSymbolCount following sync detection to continue training.
|
||||
* @param maxNoTrainSyncCount maximum number of consecutive sync detections and skipped trainings that can occur without retraining.
|
||||
*/
|
||||
public C4FMEqualizer(float[] syncSymbols, int equalizerSymbolLength, int trainingPeriodSymbolCount,
|
||||
int maxNoTrainSyncCount)
|
||||
{
|
||||
if(equalizerSymbolLength < 1)
|
||||
{
|
||||
throw new IllegalArgumentException("Equalizer symbol length must be at least 1");
|
||||
}
|
||||
|
||||
//Store the sync pattern in reverse order
|
||||
mReversedSyncSymbols = new float[syncSymbols.length];
|
||||
for(int x = 0; x < syncSymbols.length; x++)
|
||||
{
|
||||
mReversedSyncSymbols[syncSymbols.length - x - 1] = syncSymbols[x];
|
||||
}
|
||||
|
||||
//Make the symbol count odd length for equal delay on either side of the center tap
|
||||
int symbolLength = (equalizerSymbolLength % 2 == 0) ? equalizerSymbolLength - 1 : equalizerSymbolLength;
|
||||
mCenterTap = symbolLength; //This works since we're going to double the buffer length
|
||||
mDelaySymbolCount = symbolLength / 2; //Delay until new symbol is aligned to center tap
|
||||
mTaps = new float[symbolLength * 2];
|
||||
mTaps[mCenterTap] = 1.0f;
|
||||
int bufferLength = mReversedSyncSymbols.length * 2 + mTaps.length - 2;
|
||||
mBuffer = new float[bufferLength];
|
||||
mTrainingPeriodSymbolCount = trainingPeriodSymbolCount;
|
||||
mTrainingSkipCountThreshold = maxNoTrainSyncCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default equalizer span in symbols derived from the sync pattern length (odd length).
|
||||
* @param syncLength in symbols
|
||||
* @return default equalizer symbol length
|
||||
*/
|
||||
public static int getDefaultSymbolLength(int syncLength)
|
||||
{
|
||||
return (syncLength % 2 == 0) ? syncLength - 1 : syncLength;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of the current tap values
|
||||
*/
|
||||
public String getTapsDescription()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<html>EQ Taps<br>");
|
||||
for(int i = 0; i < mTaps.length; i++)
|
||||
{
|
||||
sb.append(i).append(": ").append(DF.format(mTaps[i])).append("<br>");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Symbol processing delay.
|
||||
* @return processing delay in symbols.
|
||||
*/
|
||||
public int getDelaySymbolCount()
|
||||
{
|
||||
return mDelaySymbolCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the equalizer in symbols. Note: the internal tap count is twice as long since this
|
||||
* is a fractional (x2) equalizer.
|
||||
* @return length in symbols.
|
||||
*/
|
||||
public int getLength()
|
||||
{
|
||||
return mTaps.length / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the equalizer when a valid sync pattern is detected
|
||||
*/
|
||||
public void enable()
|
||||
{
|
||||
mEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the enabled state.
|
||||
* @return enabled state
|
||||
*/
|
||||
public boolean isEnabled()
|
||||
{
|
||||
return mEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the equalizer when the signal is lost so that noise doesn't contaminate the equalizer taps.
|
||||
*/
|
||||
public void disable()
|
||||
{
|
||||
mEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the equalizer that a sync pattern is detected and queues a training sequence once a couple more
|
||||
* symbols arrive once the equalizer delay line has aligned to output the sync pattern.
|
||||
*/
|
||||
public void syncDetected()
|
||||
{
|
||||
mSyncDetected = true;
|
||||
mSymbolsUntilTraining = mDelaySymbolCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the equalizer that a sync pattern is detected and updates the sync pattern used for training. This
|
||||
* supports protocols (ie DMR) that employ multiple sync patterns of the same length.
|
||||
* @param syncSymbols for the detected sync pattern, which must be the same length as the constructor sync pattern.
|
||||
*/
|
||||
public void syncDetected(float[] syncSymbols)
|
||||
{
|
||||
if(syncSymbols.length != mReversedSyncSymbols.length)
|
||||
{
|
||||
throw new IllegalArgumentException("Sync symbols length [" + syncSymbols.length +
|
||||
"] must match the equalizer sync pattern length [" + mReversedSyncSymbols.length + "]");
|
||||
}
|
||||
|
||||
for(int x = 0; x < syncSymbols.length; x++)
|
||||
{
|
||||
mReversedSyncSymbols[syncSymbols.length - x - 1] = syncSymbols[x];
|
||||
}
|
||||
|
||||
syncDetected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the sync patter is lost and the equalizer should reset.
|
||||
*/
|
||||
public void syncLost()
|
||||
{
|
||||
// System.out.println("C4FM Equalizer - sync lost triggered");
|
||||
reset();
|
||||
disable();
|
||||
}
|
||||
|
||||
/*
|
||||
* Resets the equalizer taps
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
Arrays.fill(mTaps, 0f);
|
||||
mTaps[mCenterTap] = 1.0f;
|
||||
mVariance.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the samples and produce a delayed, equalized soft symbol ready for symbol decision. Updates the
|
||||
* equalizer taps to correct for the error in the equalized sample relative to the hard symbol decision.
|
||||
*
|
||||
* @param fractional sample that falls halfway between symbols
|
||||
* @param symbol to equalize
|
||||
* @return equalized symbol.
|
||||
*/
|
||||
public float process(float fractional, float symbol)
|
||||
{
|
||||
//Shift buffer samples to the right two places and add new sample and symbol.
|
||||
System.arraycopy(mBuffer, 0, mBuffer, 2, mBuffer.length - 2);
|
||||
mBuffer[0] = fractional;
|
||||
mBuffer[1] = symbol;
|
||||
|
||||
//Since there's a buffer delay, count down from sync detection until the pattern is aligned with the buffer
|
||||
// contents and we can start training
|
||||
if(mSyncDetected)
|
||||
{
|
||||
if(--mSymbolsUntilTraining <= 0)
|
||||
{
|
||||
train();
|
||||
}
|
||||
}
|
||||
|
||||
float equalized = 0f;
|
||||
|
||||
if(mEnabled)
|
||||
{
|
||||
int tapIndex;
|
||||
|
||||
for(tapIndex = 0; tapIndex < mTaps.length; tapIndex++)
|
||||
{
|
||||
equalized += mTaps[tapIndex] * mBuffer[tapIndex];
|
||||
}
|
||||
|
||||
if(mTrainingSymbolsRemaining > 0)
|
||||
{
|
||||
mTrainingSymbolsRemaining--;
|
||||
float error = Dibit.getError(equalized);
|
||||
error = Math.clamp(error, -MAX_SYMBOL_ERROR, MAX_SYMBOL_ERROR);
|
||||
|
||||
//Step size (mu) in range 0.0001 to 0.01
|
||||
float stepSize = 0.01f;
|
||||
|
||||
for(tapIndex = 0; tapIndex < mTaps.length; tapIndex++)
|
||||
{
|
||||
if(tapIndex != mCenterTap)
|
||||
{
|
||||
mTaps[tapIndex] += stepSize * error * mBuffer[tapIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Track the residual error that remains after equalization
|
||||
mVariance.increment(Dibit.getError(equalized));
|
||||
}
|
||||
else
|
||||
{
|
||||
//Return the uncorrected, delayed symbol when we're disabled
|
||||
equalized = mBuffer[mCenterTap];
|
||||
}
|
||||
|
||||
return equalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trains the equalizer against the buffered detected sync pattern. Trains against the sync pattern sequence
|
||||
* multiple times while the per-iteration improvement continues to become smaller and stops training once the
|
||||
* improvement falls below a threshold goal or exceeds a maximum iterations. If the training converges to
|
||||
* below a threshold, we snapshot and use the trained taps, otherwise fallback to the previous training snapshot.
|
||||
*/
|
||||
private void train()
|
||||
{
|
||||
mSyncDetected = false;
|
||||
|
||||
//Skip retrain when we're still doing dynamic tap adjustments following the previous training session.
|
||||
if(mTrainingSymbolsRemaining > 0)
|
||||
{
|
||||
mVariance.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// System.out.println("**C4FM EQ Training Triggered - skip count[" + mSkippedTrainingCount + "] variance [" + mVariance.getResult() + "] var threshold [" + TRAINING_SKIP_VARIANCE_THRESHOLD + "]");
|
||||
|
||||
//Ensure we've collected enough variance samples in the buffer and we have sufficient variance
|
||||
if(mVariance.getN() <= mDelaySymbolCount || mVariance.getResult() > TRAINING_SKIP_VARIANCE_THRESHOLD ||
|
||||
++mSkippedTrainingCount > mTrainingSkipCountThreshold)
|
||||
{
|
||||
mSkippedTrainingCount = 0;
|
||||
//If the current variance tracked since the last training session using dynamic tap adjustments at each
|
||||
// symbol is excessive, fallback to the most recent training snapshot as a better starting point
|
||||
if(mVariance.getResult() > 0.225)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
float accumulator, error;
|
||||
double improvement, previousVariance;
|
||||
int symbolIndex, index, trainingIterationCount = 0;
|
||||
Variance variance = new Variance();
|
||||
|
||||
//Step size (mu) in range 0.0001 to 0.01
|
||||
float stepSize = 0.005f;
|
||||
|
||||
do
|
||||
{
|
||||
previousVariance = variance.getN() == 0 ? 20.0 : variance.getResult();
|
||||
variance.clear();
|
||||
|
||||
|
||||
for(symbolIndex = 0; symbolIndex < mReversedSyncSymbols.length; symbolIndex++)
|
||||
{
|
||||
accumulator = 0.0f;
|
||||
for(index = 0; index < mTaps.length; index++)
|
||||
{
|
||||
accumulator += mTaps[index] * mBuffer[index + (2 * symbolIndex)];
|
||||
}
|
||||
|
||||
error = mReversedSyncSymbols[symbolIndex] - accumulator;
|
||||
variance.increment(error);
|
||||
|
||||
for(index = 0; index < mTaps.length; index++)
|
||||
{
|
||||
if(index != mCenterTap)
|
||||
{
|
||||
mTaps[index] += error * stepSize * mBuffer[index + (2 * symbolIndex)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trainingIterationCount++;
|
||||
improvement = previousVariance - variance.getResult();
|
||||
}
|
||||
while(trainingIterationCount < TRAINING_ITERATIONS_MAX && improvement > TRAINING_IMPROVEMENT_GOAL_PER_ITERATION);
|
||||
|
||||
// System.out.println("**C4FM EQ - train complete - iterations [" + trainingIterationCount +
|
||||
// "] end variance [" + DF.format(variance.getResult()) +
|
||||
// "] final improvement [" + DF.format(improvement) +
|
||||
// "] improvement threshold [" + DF.format(TRAINING_IMPROVEMENT_GOAL_PER_ITERATION) + "]");
|
||||
|
||||
//If we failed to converge, reset the taps to default and use blind training
|
||||
if(variance.getResult() > TRAINING_CONVERGENCE_THRESHOLD)
|
||||
{
|
||||
reset();
|
||||
}
|
||||
|
||||
mTrainingSymbolsRemaining = mTrainingPeriodSymbolCount;
|
||||
}
|
||||
|
||||
mVariance.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package io.github.dsheirer.dsp.c4fm;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
|
||||
/**
|
||||
* C4FM message framer base class.
|
||||
*/
|
||||
public abstract class C4FMMessageFramer
|
||||
{
|
||||
private static final float SOFT_SYMBOL_SCALE = (float)(4.0 / Math.PI); //Radians to symbol units (+/-1, +/-3)
|
||||
|
||||
/**
|
||||
* Converts the soft symbol into a pair of soft bit values using max-log likelihood ratios for the Gray mapped
|
||||
* 4-level symbol (D01=+3, D00=+1, D10=-1, D11=-3), where a positive soft bit value favors a 1 bit.
|
||||
* @param softSymbol in radians
|
||||
* @param softBits array to receive the two soft bit values
|
||||
* @param index for the first bit (sign bit), followed by the second (magnitude) bit
|
||||
*/
|
||||
protected static void setSoftBits(float softSymbol, float[] softBits, int index)
|
||||
{
|
||||
float u = softSymbol * SOFT_SYMBOL_SCALE;
|
||||
float magnitude = Math.abs(u);
|
||||
//First bit is 1 for negative symbols
|
||||
float favorZero = magnitude <= 2.0f ? 4.0f * u : 8.0f * u - 8.0f * Math.signum(u);
|
||||
softBits[index] = -favorZero;
|
||||
//Second bit is 1 for the outer (+/-3) symbols
|
||||
softBits[index + 1] = 4.0f * (magnitude - 2.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the symbol
|
||||
* @param symbol in radians
|
||||
* @return true if the framer emits a valid message that includes this symbol, so that corrective measurement
|
||||
* can be sent to a feedback decoder.
|
||||
*/
|
||||
public abstract boolean process(Dibit symbol);
|
||||
|
||||
/**
|
||||
* Process the symbol decision along with the soft symbol it was decided from. Framers that can use soft symbols
|
||||
* (ie soft decision decoding) override this method. Default implementation discards the soft symbol.
|
||||
* @param symbol decision
|
||||
* @param softSymbol equalized soft symbol in radians
|
||||
* @return true if the framer emits a valid message that includes this symbol.
|
||||
*/
|
||||
public boolean process(Dibit symbol, float softSymbol)
|
||||
{
|
||||
return process(symbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the symbol decision along with the soft symbol it was decided from and the soft symbol reliability (ie
|
||||
* inverse of the soft symbol noise variance). Framers that can weight soft decision decoding override this method.
|
||||
* Default implementation discards the reliability.
|
||||
* @param symbol decision
|
||||
* @param softSymbol equalized soft symbol in radians
|
||||
* @param reliability of the soft symbol
|
||||
* @return true if the framer emits a valid message that includes this symbol.
|
||||
*/
|
||||
public boolean process(Dibit symbol, float softSymbol, float reliability)
|
||||
{
|
||||
return process(symbol, softSymbol);
|
||||
}
|
||||
|
||||
/**
|
||||
* A sync pattern is detected
|
||||
*/
|
||||
public abstract void syncDetected();
|
||||
}
|
||||
1424
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java
Normal file
1424
src/main/java/io/github/dsheirer/dsp/c4fm/C4FMSymbolProcessor.java
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user