mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
Compare commits
45 Commits
v0.4.0-bet
...
v0.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e37f1e687 | ||
|
|
5e8f2d0523 | ||
|
|
9fb6c33319 | ||
|
|
fdecac4230 | ||
|
|
85c27a6cca | ||
|
|
57f56d31a2 | ||
|
|
df25675d3f | ||
|
|
277a84702a | ||
|
|
c651296378 | ||
|
|
866d25e86a | ||
|
|
5e62f1b889 | ||
|
|
e9d524ad14 | ||
|
|
158c8b4b8a | ||
|
|
41df207bb9 | ||
|
|
692ffabe82 | ||
|
|
944ad5a571 | ||
|
|
5ca70d3940 | ||
|
|
904752e3dd | ||
|
|
cf61798466 | ||
|
|
fd72f459f9 | ||
|
|
1bc462b300 | ||
|
|
1c6f91f101 | ||
|
|
a7c28f066a | ||
|
|
604c8ab6e5 | ||
|
|
23a3ee39cc | ||
|
|
fa16db129b | ||
|
|
de249c448b | ||
|
|
e1bc56c500 | ||
|
|
d1d115d11f | ||
|
|
aca41b4f6e | ||
|
|
1e3ae996ac | ||
|
|
b3b2e74688 | ||
|
|
44237bec5c | ||
|
|
53b44f44e3 | ||
|
|
9af7a184ad | ||
|
|
9c8d87ec64 | ||
|
|
4c34968d28 | ||
|
|
2d71f5173a | ||
|
|
2e942113c6 | ||
|
|
e20540ccc6 | ||
|
|
8d7e0e9f46 | ||
|
|
cc5d94f58d | ||
|
|
26197e433c | ||
|
|
f54b4dfd0e | ||
|
|
085ba52294 |
11
.idea/compiler.xml
generated
11
.idea/compiler.xml
generated
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel>
|
||||
<module name="sdr-trunk_main" target="11" />
|
||||
<module name="sdr-trunk_test" target="11" />
|
||||
<module name="sdrtrunk-sdr-trunk_main" target="11" />
|
||||
<module name="sdrtrunk-sdr-trunk_test" target="11" />
|
||||
</bytecodeTargetLevel>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/libraries/imports.xml
generated
12
.idea/libraries/imports.xml
generated
@@ -1,12 +0,0 @@
|
||||
<component name="libraryTable">
|
||||
<library name="imports">
|
||||
<CLASSES>
|
||||
<root url="jar://$PROJECT_DIR$/imports/java-lame-3.98.4.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/commons-math3-3.6.1.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/libresample4j-0.1.3.jar!/" />
|
||||
<root url="jar://$PROJECT_DIR$/imports/jmbe-api-1.0.0.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="11" project-jdk-type="JavaSDK">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="14" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/modules.xml
generated
12
.idea/modules.xml
generated
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdr-trunk.iml" filepath="$PROJECT_DIR$/.idea/modules/sdr-trunk.iml" group="sdr-trunk" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdr-trunk_main.iml" filepath="$PROJECT_DIR$/.idea/modules/sdr-trunk_main.iml" group="sdr-trunk" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdr-trunk_test.iml" filepath="$PROJECT_DIR$/.idea/modules/sdr-trunk_test.iml" group="sdr-trunk" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdrtrunk-sdr-trunk_main.iml" filepath="$PROJECT_DIR$/.idea/modules/sdrtrunk-sdr-trunk_main.iml" group="sdr-trunk" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/modules/sdrtrunk-sdr-trunk_test.iml" filepath="$PROJECT_DIR$/.idea/modules/sdrtrunk-sdr-trunk_test.iml" group="sdr-trunk" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/vcs.xml
generated
12
.idea/vcs.xml
generated
@@ -1,16 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="IssueNavigationConfiguration">
|
||||
<option name="links">
|
||||
<list>
|
||||
<IssueNavigationLink>
|
||||
<option name="issueRegexp" value="#(\d+)" />
|
||||
<option name="linkRegexp" value="https://github.com/DSheirer/sdrtrunk/issues/$1" />
|
||||
</IssueNavigationLink>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
15
CHANGELOG
15
CHANGELOG
@@ -1,3 +1,18 @@
|
||||
0.4.0 20200404
|
||||
- Release includes embedded Java 14 & JavaFX 13 runtime
|
||||
- Adds Polyphase Channelizer with 2x channel oversampling and redesigned source management
|
||||
- Adds P25 Phase 2 decoder
|
||||
- Adds P25 radio identifier aliasing
|
||||
- JMBE library 1.0.4 with support for AMBE tone metadata
|
||||
- New JavaFX User Preferences dialog
|
||||
- HackRF tuner enhanced (sample rates and saturation issues)
|
||||
- Adds support for Airspy mini tuner
|
||||
- Redesigned audio sub-system that uses AudioSegments
|
||||
- USB tuner bus saturation management/monitoring
|
||||
- Bitstream (.bits) recording
|
||||
- MP3 Audio Recording
|
||||
- Numerous bug fixes
|
||||
|
||||
0.3.4 20180418
|
||||
- New P25 C4FM decoder that uses Differential QPSK decoder instead of FM demodulation with 4FSK.
|
||||
- Adds channel auto-start and ordering capability with startup dialog allowing user to invoke or bypass channel
|
||||
|
||||
261
build.gradle
261
build.gradle
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -23,17 +23,18 @@ plugins {
|
||||
id 'application'
|
||||
id 'java'
|
||||
id 'idea'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.6'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.8'
|
||||
id 'org.beryx.runtime' version '1.8.0'
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
|
||||
version = '0.4.0-beta.2'
|
||||
sourceCompatibility = '11'
|
||||
version = '0.4.0'
|
||||
sourceCompatibility = '13'
|
||||
|
||||
sourceSets {
|
||||
main.java.srcDirs 'src/main'
|
||||
@@ -41,45 +42,56 @@ sourceSets {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile 'org.assertj:assertj-core:3.8.0'
|
||||
compile fileTree(dir: 'imports', include: '*.jar')
|
||||
compile 'ch.qos.logback:logback-core:1.2.3'
|
||||
compile 'ch.qos.logback:logback-classic:1.2.3'
|
||||
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
|
||||
compile 'com.fazecast:jSerialComm:2.5.0'
|
||||
compile 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
compile 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
||||
compile 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
compile 'com.github.wendykierp:JTransforms:3.1'
|
||||
compile 'com.google.guava:guava:27.0.1-jre'
|
||||
compile 'com.jidesoft:jide-oss:3.6.18'
|
||||
compile 'com.miglayout:miglayout-swing:5.2'
|
||||
compile 'eu.hansolo:charts:1.0.5'
|
||||
compile 'io.github.dsheirer:radio-reference-api:15.1.2'
|
||||
compile 'javax.usb:usb-api:1.0.2'
|
||||
compile 'net.coderazzi:tablefilter-swing:5.4.0'
|
||||
compile 'org.apache.commons:commons-lang3:3.8.1'
|
||||
compile 'org.apache.commons:commons-io:1.3.2'
|
||||
compile 'org.apache.mina:mina-core:2.0.19'
|
||||
compile 'org.apache.mina:mina-http:2.0.19'
|
||||
compile "org.openjfx:javafx-base:11:win"
|
||||
compile "org.openjfx:javafx-base:11:linux"
|
||||
compile "org.openjfx:javafx-base:11:mac"
|
||||
compile "org.openjfx:javafx-controls:11.0.2:win"
|
||||
compile "org.openjfx:javafx-controls:11.0.2:linux"
|
||||
compile "org.openjfx:javafx-controls:11.0.2:mac"
|
||||
compile "org.openjfx:javafx-graphics:11.0.2:win"
|
||||
compile "org.openjfx:javafx-graphics:11.0.2:linux"
|
||||
compile "org.openjfx:javafx-graphics:11.0.2:mac"
|
||||
compile "org.openjfx:javafx-swing:11.0.2:win"
|
||||
compile "org.openjfx:javafx-swing:11.0.2:linux"
|
||||
compile "org.openjfx:javafx-swing:11.0.2:mac"
|
||||
compile 'org.slf4j:slf4j-api:1.7.25'
|
||||
compile 'org.usb4java:libusb4java:1.3.0'
|
||||
compile 'org.usb4java:usb4java:1.3.0'
|
||||
compile 'org.usb4java:usb4java-javax:1.3.0'
|
||||
compile 'pl.edu.icm:JLargeArrays:1.6'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.assertj:assertj-core:3.8.0'
|
||||
|
||||
//Jitpack imports
|
||||
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||
implementation 'com.github.dnault:libresample4j:master'
|
||||
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
|
||||
|
||||
//mavenCentral/jcenter imports
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'ch.qos.logback:logback-core:1.2.3'
|
||||
implementation 'ch.qos.logback:logback-classic:1.2.3'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
|
||||
implementation 'com.fazecast:jSerialComm:2.5.0'
|
||||
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.guava:guava:27.0.1-jre'
|
||||
implementation 'com.jidesoft:jide-oss:3.6.18'
|
||||
implementation 'com.miglayout:miglayout-swing:5.2'
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'eu.hansolo:charts:1.0.5'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:15.1.2'
|
||||
implementation 'javax.usb:usb-api:1.0.2'
|
||||
implementation 'net.coderazzi:tablefilter-swing:5.4.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.8.1'
|
||||
implementation 'org.apache.commons:commons-io:1.3.2'
|
||||
implementation 'org.apache.mina:mina-core:2.0.19'
|
||||
implementation 'org.apache.mina:mina-http:2.0.19'
|
||||
implementation 'org.controlsfx:controlsfx:11.0.1'
|
||||
implementation 'org.slf4j:slf4j-api:1.7.25'
|
||||
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java-javax:1.3.0'
|
||||
implementation 'pl.edu.icm:JLargeArrays:1.6'
|
||||
|
||||
//The must remain as compile options to ensure they are included in the build to produce multi-OS images
|
||||
compile "org.openjfx:javafx-base:13:win"
|
||||
compile "org.openjfx:javafx-base:13:linux"
|
||||
compile "org.openjfx:javafx-base:13:mac"
|
||||
compile "org.openjfx:javafx-controls:13.0.1:win"
|
||||
compile "org.openjfx:javafx-controls:13.0.1:linux"
|
||||
compile "org.openjfx:javafx-controls:13.0.1:mac"
|
||||
compile "org.openjfx:javafx-graphics:13.0.1:win"
|
||||
compile "org.openjfx:javafx-graphics:13.0.1:linux"
|
||||
compile "org.openjfx:javafx-graphics:13.0.1:mac"
|
||||
compile "org.openjfx:javafx-swing:13.0.1:win"
|
||||
compile "org.openjfx:javafx-swing:13.0.1:linux"
|
||||
compile "org.openjfx:javafx-swing:13.0.1:mac"
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -100,128 +112,117 @@ javafx {
|
||||
jar {
|
||||
manifest {
|
||||
attributes (
|
||||
'Implementation-Title' : 'sdrtrunk project',
|
||||
'Implementation-Version' : archiveVersion,
|
||||
'Class-Path' : 'jmbe-1.0.1.jar',
|
||||
'Created-By' : "Gradle ${gradle.gradleVersion}",
|
||||
'Build-Timestamp' : new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
|
||||
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
|
||||
'Build-OS' : "${System.properties['os.name']} (${System.properties['os.arch']} ${System.properties['os.version']}"
|
||||
'Implementation-Title' : 'sdrtrunk project',
|
||||
'Implementation-Version' : archiveVersion,
|
||||
'Class-Path' : 'jmbe-1.0.4.jar',
|
||||
'Created-By' : "Gradle ${gradle.gradleVersion}",
|
||||
'Build-Timestamp' : new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
|
||||
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
|
||||
'Build-OS' : "${System.properties['os.name']} (${System.properties['os.arch']} ${System.properties['os.version']}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Java Development Kit (JDK) locations. These paths must point to the 'bin' directory
|
||||
* within a JDK for each of the specified architectures. These JDKs are used by the
|
||||
* runtime and runtimeZip tasks to produce platform-specific builds
|
||||
* Java Development Kit (JDK) locations. In order to build OS-specific images, these paths must point to the 'bin'
|
||||
* directory within a JDK for each of the specified architectures. These JDKs are used by the runtime and runtimeZip
|
||||
* tasks to produce platform-specific builds. If none of these paths exists, then an image will be created for the
|
||||
* host OS using the installed JDK.
|
||||
*/
|
||||
def jdkLinux_x64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-13.0.1'
|
||||
def jdkLinux_x86 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-13.0.1'
|
||||
def jdkOsx_x64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-13.0.1.jdk'
|
||||
def jdkWindows_x64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-13.0.1'
|
||||
def jdkWindows_x86 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-13.0.1'
|
||||
//def jdkLinux_aarch64 = '/media/denny/WD250GB/java/bellsoft/linux-aarch64/jdk-11.0.2'
|
||||
//def jdkLinux_arm32 = '/media/denny/WD250GB/java/bellsoft/linux-arm32/jdk-11.0.2'
|
||||
//def jfxLinux_arm32 = '/media/denny/WD250GB/java/bellsoft/linux-arm32/armv6hf-sdk/lib'
|
||||
def jdk_linux_x86_64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-14'
|
||||
def jdk_linux_x86_32 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-14'
|
||||
def jdk_osx_x86_64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-14.jdk'
|
||||
def jdk_windows_x86_64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-14'
|
||||
def jdk_windows_x86_32 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-14'
|
||||
|
||||
def file_jdk_linux_x86_64 = new File(jdk_linux_x86_64)
|
||||
def file_jdk_linux_x86_32 = new File(jdk_linux_x86_32)
|
||||
def file_jdk_osx_x86_64 = new File(jdk_osx_x86_64)
|
||||
def file_jdk_windows_x86_64 = new File(jdk_windows_x86_64)
|
||||
def file_jdk_windows_x86_32 = new File(jdk_windows_x86_32)
|
||||
|
||||
def hasTargetJdk = file_jdk_linux_x86_32.exists() ||
|
||||
file_jdk_linux_x86_64.exists() ||
|
||||
file_jdk_osx_x86_64.exists() ||
|
||||
file_jdk_windows_x86_32.exists() ||
|
||||
file_jdk_windows_x86_64.exists()
|
||||
|
||||
runtime {
|
||||
targetPlatform('linux-x86_64', jdkLinux_x64)
|
||||
targetPlatform('linux-x86_32', jdkLinux_x86)
|
||||
targetPlatform('osx-x86_64', jdkOsx_x64)
|
||||
targetPlatform('windows-x86_64', jdkWindows_x64)
|
||||
targetPlatform('windows-x86_32', jdkWindows_x86)
|
||||
// targetPlatform('linux-aarch64', jdkLinux_aarch64)
|
||||
// targetPlatform('linux-arm32', jdkLinux_arm32)
|
||||
if(file_jdk_linux_x86_64.exists())
|
||||
{
|
||||
targetPlatform('linux-x86_64-v' + version, jdk_linux_x86_64)
|
||||
}
|
||||
|
||||
if(file_jdk_linux_x86_32.exists())
|
||||
{
|
||||
targetPlatform('linux-x86_32-v' + version, jdk_linux_x86_32)
|
||||
}
|
||||
|
||||
if(file_jdk_osx_x86_64.exists())
|
||||
{
|
||||
targetPlatform('osx-x86_64-v' + version, jdk_osx_x86_64)
|
||||
}
|
||||
|
||||
if(file_jdk_windows_x86_64.exists())
|
||||
{
|
||||
targetPlatform('windows-x86_64-v' + version, jdk_windows_x86_64)
|
||||
}
|
||||
|
||||
if(file_jdk_windows_x86_32.exists())
|
||||
{
|
||||
targetPlatform('windows-x86_32-v' + version, jdk_windows_x86_32)
|
||||
}
|
||||
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
|
||||
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop']
|
||||
imageZip = file("$buildDir/image/sdr-trunk-" + version + ".zip")
|
||||
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop', 'java.net.http']
|
||||
imageZip = hasTargetJdk ? file("$buildDir/image/sdr-trunk.zip") : file("$buildDir/image/sdr-trunk-" + version + ".zip")
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for existence of JDK folders for each supported platform before creating runtime images
|
||||
*/
|
||||
tasks.runtime.doFirst {
|
||||
def linux_x64 = new File(jdkLinux_x64)
|
||||
|
||||
if(!linux_x64.exists())
|
||||
if(!file_jdk_linux_x86_64.exists())
|
||||
{
|
||||
println("Linux x64 JDK was not found at " + jdkLinux_x64)
|
||||
throw new GradleException("Cannot find Java Development Kit (JDK) for linux-x64 architecture. " +
|
||||
"Please update the build.gradle script to provide the correct path")
|
||||
println("Skipping OS Image - Linux x86 64-bit JDK was not found at " + jdk_linux_x86_64)
|
||||
}
|
||||
|
||||
def linux_x86 = new File(jdkLinux_x86)
|
||||
|
||||
if(!linux_x86.exists())
|
||||
if(!file_jdk_linux_x86_32.exists())
|
||||
{
|
||||
println("Linux x86 JDK was not found at " + jdkLinux_x86)
|
||||
throw new GradleException("Cannot find Java Development Kit (JDK) for linux-x86 32-bit architecture. " +
|
||||
"Please update the build.gradle script to provide the correct path")
|
||||
println("Skipping OS Image - Linux x86 32-bit JDK was not found at " + jdk_linux_x86_32)
|
||||
}
|
||||
|
||||
def osx_x64 = new File(jdkOsx_x64)
|
||||
|
||||
if(!osx_x64.exists())
|
||||
if(!file_jdk_osx_x86_64.exists())
|
||||
{
|
||||
println("OSX x64 JDK was not found at " + jdkOsx_x64);
|
||||
throw new GradleException("Cannot find Java Development Kit (JDK) for osx-x64 architecture. " +
|
||||
"Please update the build.gradle script to provide the correct path")
|
||||
println("Skipping OS Image - OSX x86 64-bit JDK was not found at " + jdk_osx_x86_64);
|
||||
}
|
||||
|
||||
def windows_x64 = new File(jdkWindows_x64)
|
||||
|
||||
if(!windows_x64.exists())
|
||||
if(!file_jdk_windows_x86_64.exists())
|
||||
{
|
||||
println("Windows x64 JDK was not found at " + jdkWindows_x64)
|
||||
throw new GradleException("Cannot find Java Development Kit (JDK) for windows-x64 architecture. " +
|
||||
"Please update the build.gradle script to provide the correct path")
|
||||
println("Skipping OS Image - Windows x86 64-bit JDK was not found at " + jdk_windows_x86_64)
|
||||
}
|
||||
|
||||
def windows_x86 = new File(jdkWindows_x86)
|
||||
|
||||
if(!windows_x86.exists())
|
||||
if(!file_jdk_windows_x86_32.exists())
|
||||
{
|
||||
println("Windows x86 32-bit JDK was not found at " + jdkWindows_x86)
|
||||
throw new GradleException("Cannot find Java Development Kit (JDK) for windows-x64 architecture. " +
|
||||
"Please update the build.gradle script to provide the correct path")
|
||||
println("Skipping OS Image - Windows x86 32-bit JDK was not found at " + jdk_windows_x86_32)
|
||||
}
|
||||
|
||||
// def linux_aarch64 = new File(jdkLinux_aarch64)
|
||||
//
|
||||
// if(!linux_aarch64.exists())
|
||||
// {
|
||||
// println("Linux aarch64 JDK was not found at " + jdkLinux_aarch64)
|
||||
// throw new GradleException("Cannot find Java Development Kit (JDK) for linux-aarch64 architecture. " +
|
||||
// "Please update the build.gradle script to provide the correct path")
|
||||
// }
|
||||
//
|
||||
// def linux_arm32 = new File(jdkLinux_arm32)
|
||||
//
|
||||
// if(!linux_arm32.exists())
|
||||
// {
|
||||
// println("Linux arm32 JDK was not found at " + jdkLinux_arm32)
|
||||
// throw new GradleException("Cannot find Java Development Kit (JDK) for linux-arm32 architecture. " +
|
||||
// "Please update the build.gradle script to provide the correct path")
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we have to list each of the platform-specific JavaFX libraries as dependencies,
|
||||
* remove them from each build image
|
||||
* Remove unused JavaFX OS-specific libraries from each platform target directory
|
||||
*/
|
||||
tasks.runtime.doLast {
|
||||
delete(fileTree('build/image/sdr-trunk-linux-x86_64/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-linux-x86_32/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-osx-x86_64/lib').include { it.name ==~ /javafx.*-(win|linux)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-windows-x86_64/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-windows-x86_32/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-linux-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-linux-x86_32-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-osx-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|linux)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-windows-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-windows-x86_32-v' + version + '/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
|
||||
|
||||
// delete(fileTree('build/image/sdr-trunk-linux-aarch64/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
|
||||
// delete(fileTree('build/image/sdr-trunk-linux-arm32/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
|
||||
// copy {
|
||||
// from(jfxLinux_arm32).exclude("src.zip")
|
||||
// into 'build/image/sdr-trunk-linux-arm32/lib'
|
||||
// }
|
||||
//JavaFX web and media modules are transitive dependencies of ControlsFX but are not required for this application
|
||||
delete(fileTree('build/image/sdr-trunk-linux-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-linux-x86_32-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-osx-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-windows-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||
delete(fileTree('build/image/sdr-trunk-windows-x86_32-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||
}
|
||||
|
||||
16
copyright.template
Normal file
16
copyright.template
Normal file
@@ -0,0 +1,16 @@
|
||||
******************************************************************************
|
||||
Copyright (C) 2014-${today.year} Dennis Sheirer
|
||||
|
||||
This 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/>
|
||||
*****************************************************************************
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
24
gradle/wrapper/gradle-wrapper.properties
vendored
24
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,27 +1,5 @@
|
||||
#
|
||||
# /*
|
||||
# * ******************************************************************************
|
||||
# * 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/>
|
||||
# * *****************************************************************************
|
||||
# */
|
||||
#
|
||||
#
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
51
gradlew
vendored
51
gradlew
vendored
@@ -1,5 +1,21 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m"'
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
@@ -109,8 +125,8 @@ if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
@@ -138,19 +154,19 @@ if $cygwin ; then
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -159,14 +175,9 @@ save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
APP_ARGS=`save "$@"`
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
18
gradlew.bat
vendored
18
gradlew.bat
vendored
@@ -1,3 +1,19 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m"
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -30,6 +30,7 @@
|
||||
<component name="DependencyValidationManager">
|
||||
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
||||
</component>
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
@@ -60,21 +61,7 @@
|
||||
<version value="1.0" />
|
||||
</component>
|
||||
<component name="JavadocGenerationManager">
|
||||
<option name="OUTPUT_DIRECTORY" />
|
||||
<option name="OPTION_SCOPE" value="protected" />
|
||||
<option name="OPTION_HIERARCHY" value="true" />
|
||||
<option name="OPTION_NAVIGATOR" value="true" />
|
||||
<option name="OPTION_INDEX" value="true" />
|
||||
<option name="OPTION_SEPARATE_INDEX" value="true" />
|
||||
<option name="OPTION_DOCUMENT_TAG_USE" value="false" />
|
||||
<option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false" />
|
||||
<option name="OPTION_DOCUMENT_TAG_VERSION" value="false" />
|
||||
<option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true" />
|
||||
<option name="OPTION_DEPRECATED_LIST" value="true" />
|
||||
<option name="OTHER_OPTIONS" value="" />
|
||||
<option name="HEAP_SIZE" />
|
||||
<option name="LOCALE" />
|
||||
<option name="OPEN_IN_BROWSER" value="true" />
|
||||
</component>
|
||||
<component name="Palette2">
|
||||
<group name="Swing">
|
||||
|
||||
@@ -538,9 +538,12 @@ public class AliasList implements Listener<AliasEvent>
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
mLog.warn("Alias [" + alias.getName() + "] talkgroup [" + talkgroup.getValue() +
|
||||
"] has the same talkgroup value as alias [" + existing.getName() +
|
||||
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
mLog.warn("Alias [" + alias.getName() + "] talkgroup [" + talkgroup.getValue() +
|
||||
"] has the same talkgroup value as alias [" + existing.getName() +
|
||||
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||
@@ -551,7 +554,7 @@ public class AliasList implements Listener<AliasEvent>
|
||||
//Log warning if the new talkgroup range overlaps with any existing ranges
|
||||
for(Map.Entry<TalkgroupRange,Alias> entry: mTalkgroupRangeAliasMap.entrySet())
|
||||
{
|
||||
if(talkgroupRange.overlaps(entry.getKey()))
|
||||
if(talkgroupRange.overlaps(entry.getKey()) && !entry.getValue().equals(alias))
|
||||
{
|
||||
mLog.warn("Alias [" + alias.getName() + "] with talkgroup range [" + talkgroupRange.toString() +
|
||||
"] overlaps with alias [" + entry.getValue().getName() +
|
||||
@@ -621,9 +624,12 @@ public class AliasList implements Listener<AliasEvent>
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
mLog.warn("Alias [" + alias.getName() + "] radio ID [" + radio.getValue() +
|
||||
"] has the same value as alias [" + existing.getName() +
|
||||
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
mLog.warn("Alias [" + alias.getName() + "] radio ID [" + radio.getValue() +
|
||||
"] has the same value as alias [" + existing.getName() +
|
||||
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
mRadioAliasMap.put(radio.getValue(), alias);
|
||||
@@ -634,7 +640,7 @@ public class AliasList implements Listener<AliasEvent>
|
||||
//Log warning if the new range overlaps with any existing ranges
|
||||
for(Map.Entry<RadioRange,Alias> entry: mRadioRangeAliasMap.entrySet())
|
||||
{
|
||||
if(radioRange.overlaps(entry.getKey()))
|
||||
if(radioRange.overlaps(entry.getKey()) && !entry.getValue().equals(alias))
|
||||
{
|
||||
mLog.warn("Alias [" + alias.getName() + "] with radio ID range [" + radioRange.toString() +
|
||||
"] overlaps with alias [" + entry.getValue().getName() +
|
||||
|
||||
@@ -78,8 +78,8 @@ public class Radio extends AliasID
|
||||
return false;
|
||||
}
|
||||
|
||||
TalkgroupFormat talkgroupFormat = TalkgroupFormat.get(mProtocol);
|
||||
return talkgroupFormat.getMinimumValidValue() <= mValue && mValue <= talkgroupFormat.getMaximumValidValue();
|
||||
RadioFormat radioFormat = RadioFormat.get(mProtocol);
|
||||
return radioFormat.getMinimumValidValue() <= mValue && mValue <= radioFormat.getMaximumValidValue();
|
||||
}
|
||||
|
||||
public String toString()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -34,8 +34,8 @@ import io.github.dsheirer.protocol.Protocol;
|
||||
*/
|
||||
public enum RadioFormat
|
||||
{
|
||||
APCO25("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||
"<html>APCO25 valid range is 1 to 16,777,215"),
|
||||
APCO25("********", 0, 0xFFFFFF, "0 to 16,777,215",
|
||||
"<html>APCO25 valid range is 0 to 16,777,215"),
|
||||
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||
"Unknown protocol valid value range is 1-16,777,215");
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* 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.
|
||||
* * ******************************************************************************
|
||||
* * 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 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;
|
||||
@@ -32,8 +34,8 @@ import io.github.dsheirer.protocol.Protocol;
|
||||
*/
|
||||
public enum TalkgroupFormat
|
||||
{
|
||||
APCO25("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||
"<html>APCO25 valid range is 1 to 65,535(talkgroup)<br>or 1 to 16,777,215 (radio ID)"),
|
||||
APCO25("********", 0, 0xFFFFFF, "0 to 65,535",
|
||||
"<html>APCO25 talkgroup valid range is 0 to 65,535"),
|
||||
FLEETSYNC("###-####", 1, 0x7FFFFF, "001-0001 to 127-8192",
|
||||
"<html>Fleetsync valid ranges are 1-127(prefix)<br>and 1-8192(ident) (ie. 001-0001 to 127-8192)"),
|
||||
LTR("##-###", 257, 5375, "01-001 to 20-255",
|
||||
|
||||
@@ -20,41 +20,134 @@
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base audio module implementation.
|
||||
*/
|
||||
public abstract class AbstractAudioModule extends Module implements IAudioPacketProvider, IdentifierUpdateListener,
|
||||
ISquelchStateListener
|
||||
public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener
|
||||
{
|
||||
//Static unique audio channel identifier.
|
||||
private static int AUDIO_CHANNEL_ID_GENERATOR = 1;
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class);
|
||||
private static final int MAX_SEGMENT_AUDIO_SAMPLE_LENGTH = 8000 * 60 * 1; //8 kHz - 1 minute
|
||||
|
||||
private int mAudioChannelId = AUDIO_CHANNEL_ID_GENERATOR++;
|
||||
private Listener<ReusableAudioPacket> mAudioPacketListener;
|
||||
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
|
||||
private Listener<AudioSegment> mAudioSegmentListener;
|
||||
protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
|
||||
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
|
||||
private AliasList mAliasList;
|
||||
private AudioSegment mAudioSegment;
|
||||
private int mAudioSampleCount = 0;
|
||||
private boolean mRecordAudioOverride;
|
||||
|
||||
/**
|
||||
* Constructs an abstract audio module
|
||||
*/
|
||||
public AbstractAudioModule()
|
||||
public AbstractAudioModule(AliasList aliasList)
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
|
||||
}
|
||||
|
||||
protected abstract int getTimeslot();
|
||||
|
||||
/**
|
||||
* Closes the current audio segment
|
||||
*/
|
||||
protected void closeAudioSegment()
|
||||
{
|
||||
synchronized(this)
|
||||
{
|
||||
if(mAudioSegment != null)
|
||||
{
|
||||
mAudioSegment.completeProperty().set(true);
|
||||
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
|
||||
mAudioSegment = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
closeAudioSegment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique channel identifier for use in tagging audio packets with an audio channel ID so that they can
|
||||
* be identified within a combined audio packet stream
|
||||
* Gets the current audio segment, or creates a new audio segment as necessary and broadcasts it to any registered
|
||||
* listener(s).
|
||||
*/
|
||||
public int getAudioChannelId()
|
||||
protected AudioSegment getAudioSegment()
|
||||
{
|
||||
return mAudioChannelId;
|
||||
synchronized(this)
|
||||
{
|
||||
if(mAudioSegment == null)
|
||||
{
|
||||
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
|
||||
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
|
||||
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
|
||||
|
||||
if(mRecordAudioOverride)
|
||||
{
|
||||
mAudioSegment.recordAudioProperty().set(true);
|
||||
}
|
||||
|
||||
if(mAudioSegmentListener != null)
|
||||
{
|
||||
mAudioSegment.incrementConsumerCount();
|
||||
mAudioSegmentListener.receive(mAudioSegment);
|
||||
}
|
||||
|
||||
mAudioSampleCount = 0;
|
||||
}
|
||||
|
||||
return mAudioSegment;
|
||||
}
|
||||
}
|
||||
|
||||
protected void addAudio(float[] audioBuffer)
|
||||
{
|
||||
AudioSegment audioSegment = getAudioSegment();
|
||||
|
||||
//If the current segment exceeds the max samples length, close it so that a new segment gets generated
|
||||
//and then link the segments together
|
||||
if(mAudioSampleCount >= MAX_SEGMENT_AUDIO_SAMPLE_LENGTH)
|
||||
{
|
||||
AudioSegment previous = getAudioSegment();
|
||||
closeAudioSegment();
|
||||
audioSegment = getAudioSegment();
|
||||
audioSegment.linkTo(previous);
|
||||
}
|
||||
|
||||
audioSegment.addAudio(audioBuffer);
|
||||
mAudioSampleCount += audioBuffer.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all audio segments as recordable when the argument is true. Otherwise, defers to the aliased identifiers
|
||||
* from the identifier collection to determine whether to record the audio or not.
|
||||
* @param recordAudio set to true to mark all audio as recordable.
|
||||
*/
|
||||
public void setRecordAudio(boolean recordAudio)
|
||||
{
|
||||
mRecordAudioOverride = recordAudio;
|
||||
|
||||
if(mRecordAudioOverride)
|
||||
{
|
||||
synchronized(this)
|
||||
{
|
||||
if(mAudioSegment != null)
|
||||
{
|
||||
mAudioSegment.recordAudioProperty().set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +156,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioPacket
|
||||
@Override
|
||||
public Listener<IdentifierUpdateNotification> getIdentifierUpdateListener()
|
||||
{
|
||||
return mIdentifierCollection;
|
||||
return mIdentifierUpdateNotificationBroadcaster;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,37 +168,20 @@ public abstract class AbstractAudioModule extends Module implements IAudioPacket
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an audio packet listener to receive the output from this audio module.
|
||||
* Registers an audio segment listener to receive the output from this audio module.
|
||||
*/
|
||||
@Override
|
||||
public void setAudioPacketListener(Listener<ReusableAudioPacket> listener)
|
||||
public void setAudioSegmentListener(Listener<AudioSegment> listener)
|
||||
{
|
||||
mAudioPacketListener = listener;
|
||||
mAudioSegmentListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the audio packet listener from receiving audio packets from this module.
|
||||
* Unregisters the audio segment listener from receiving audio segments from this module.
|
||||
*/
|
||||
@Override
|
||||
public void removeAudioPacketListener()
|
||||
public void removeAudioSegmentListener()
|
||||
{
|
||||
mAudioPacketListener = null;
|
||||
mAudioSegmentListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered listener for receiving audio packets produced by this module
|
||||
*/
|
||||
public Listener<ReusableAudioPacket> getAudioPacketListener()
|
||||
{
|
||||
return mAudioPacketListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there is a listener registered to receive audio packets from this audio module.
|
||||
*/
|
||||
public boolean hasAudioPacketListener()
|
||||
{
|
||||
return mAudioPacketListener != null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Processes audio packets to assign attributes to each packet based on alias list settings. Updates the
|
||||
* monitoring, recording and streaming attributes.
|
||||
*/
|
||||
public class AudioMetadataProcessor
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioMetadataProcessor.class);
|
||||
private AliasModel mAliasModel;
|
||||
private Map<Integer, AliasList> mAliasListMap = new HashMap<>();
|
||||
private Set<Integer> mAliasListRetrievedSet = new TreeSet<>();
|
||||
|
||||
public AudioMetadataProcessor(AliasModel aliasModel)
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
public void process(ReusableAudioPacket audioPacket)
|
||||
{
|
||||
if(audioPacket.hasIdentifierCollection() && audioPacket.getIdentifierCollection().hasAliasListConfiguration())
|
||||
{
|
||||
AliasList aliasList = null;
|
||||
|
||||
if(mAliasListRetrievedSet.contains(audioPacket.getAudioChannelId()))
|
||||
{
|
||||
aliasList = mAliasListMap.get(audioPacket.getAudioChannelId());
|
||||
}
|
||||
else
|
||||
{
|
||||
aliasList = mAliasModel.getAliasList(audioPacket.getIdentifierCollection().getAliasListConfiguration());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
mAliasListMap.put(audioPacket.getAudioChannelId(), aliasList);
|
||||
mAliasListRetrievedSet.add(audioPacket.getAudioChannelId());
|
||||
}
|
||||
}
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
audioPacket.addBroadcastChannels(aliasList.getBroadcastChannels(audioPacket.getIdentifierCollection()));
|
||||
audioPacket.setMonitoringPriority(aliasList.getAudioPlaybackPriority(audioPacket.getIdentifierCollection()));
|
||||
|
||||
//If the audio packet is already marked recordable, leave it alone, otherwise attempt to determine
|
||||
//if we should record the audio packet from the aliased identifiers
|
||||
if(!audioPacket.isRecordable())
|
||||
{
|
||||
audioPacket.setRecordable(aliasList.isRecordable(audioPacket.getIdentifierCollection()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
@@ -29,23 +31,19 @@ import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IReusableBufferListener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Provides packaging of demodulated audio sample buffers into audio packets for broadcast to registered audio packet
|
||||
* listeners. Includes audio packet metadata in constructed audio packets.
|
||||
* Provides packaging of demodulated audio sample buffers into audio segments for broadcast to registered listeners.
|
||||
* Includes audio packet metadata in constructed audio segments.
|
||||
*
|
||||
* Incorporates audio squelch state listener to control if audio packets are broadcast or ignored.
|
||||
*
|
||||
* This class is designed to support 8 kHz sample rate demodulated audio.
|
||||
*/
|
||||
public class AudioModule extends AbstractAudioModule implements IReusableBufferListener, Listener<ReusableFloatBuffer>
|
||||
public class AudioModule extends AbstractAudioModule implements ISquelchStateListener, IReusableBufferListener,
|
||||
Listener<ReusableFloatBuffer>
|
||||
{
|
||||
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
|
||||
@@ -75,36 +73,31 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
|
||||
}
|
||||
}
|
||||
|
||||
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("AudioModule");
|
||||
private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients);
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||
private boolean mRecordAudioOverride;
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
*/
|
||||
public AudioModule()
|
||||
public AudioModule(AliasList aliasList)
|
||||
{
|
||||
super(aliasList);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getTimeslot()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
removeAudioPacketListener();
|
||||
removeAudioSegmentListener();
|
||||
mSquelchStateListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all audio packets as recordable when the argument is true. Otherwise, defers to the aliased identifiers
|
||||
* from the identifier collection to determine whether to record the audio or not.
|
||||
* @param recordAudio set to true to mark all audio as recordable.
|
||||
*/
|
||||
public void setRecordAudio(boolean recordAudio)
|
||||
{
|
||||
mRecordAudioOverride = recordAudio;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset()
|
||||
{
|
||||
@@ -114,30 +107,8 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
/* No start operations provided */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
/* Issue an end-audio packet in case a recorder is still rolling */
|
||||
if(hasAudioPacketListener())
|
||||
{
|
||||
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
|
||||
endAudioPacket.resetAttributes();
|
||||
endAudioPacket.setAudioChannelId(getAudioChannelId());
|
||||
endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf());
|
||||
|
||||
if(mRecordAudioOverride)
|
||||
{
|
||||
endAudioPacket.setRecordable(true);
|
||||
}
|
||||
endAudioPacket.incrementUserCount();
|
||||
getAudioPacketListener().receive(endAudioPacket);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Listener<SquelchStateEvent> getSquelchStateListener()
|
||||
{
|
||||
@@ -147,23 +118,11 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
|
||||
@Override
|
||||
public void receive(ReusableFloatBuffer reusableFloatBuffer)
|
||||
{
|
||||
if(hasAudioPacketListener() && mSquelchState == SquelchState.UNSQUELCH)
|
||||
if(mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
ReusableFloatBuffer highPassFiltered = mHighPassFilter.filter(reusableFloatBuffer);
|
||||
|
||||
ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount());
|
||||
audioPacket.resetAttributes();
|
||||
audioPacket.setAudioChannelId(getAudioChannelId());
|
||||
audioPacket.loadAudioFrom(highPassFiltered);
|
||||
if(mRecordAudioOverride)
|
||||
{
|
||||
audioPacket.setRecordable(true);
|
||||
}
|
||||
audioPacket.setIdentifierCollection(getIdentifierCollection().copyOf());
|
||||
|
||||
getAudioPacketListener().receive(audioPacket);
|
||||
|
||||
highPassFiltered.decrementUserCount();
|
||||
ReusableFloatBuffer buffer = mHighPassFilter.filter(reusableFloatBuffer);
|
||||
addAudio(buffer.getSamples());
|
||||
buffer.decrementUserCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -186,16 +145,12 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
|
||||
@Override
|
||||
public void receive(SquelchStateEvent event)
|
||||
{
|
||||
if(event.getSquelchState() == SquelchState.SQUELCH && hasAudioPacketListener())
|
||||
{
|
||||
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
|
||||
endAudioPacket.resetAttributes();
|
||||
endAudioPacket.setAudioChannelId(getAudioChannelId());
|
||||
endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf());
|
||||
getAudioPacketListener().receive(endAudioPacket);
|
||||
}
|
||||
|
||||
mSquelchState = event.getSquelchState();
|
||||
|
||||
if(mSquelchState == SquelchState.SQUELCH)
|
||||
{
|
||||
closeAudioSegment();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.ContinuousReusableBufferProcessor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferBroadcaster;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Processes audio packets from all decoder channels, assigns alias attributes and distributes packets
|
||||
* for playback, streaming and recording.
|
||||
*/
|
||||
public class AudioPacketManager implements Listener<ReusableAudioPacket>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioPacketManager.class);
|
||||
private AudioMetadataProcessor mAudioMetadataProcessor;
|
||||
private ReusableBufferBroadcaster<ReusableAudioPacket> mBroadcaster = new ReusableBufferBroadcaster<>();
|
||||
private ContinuousReusableBufferProcessor<ReusableAudioPacket> mBufferProcessor;
|
||||
|
||||
public AudioPacketManager(AliasModel aliasModel)
|
||||
{
|
||||
mAudioMetadataProcessor = new AudioMetadataProcessor(aliasModel);
|
||||
mBufferProcessor = new ContinuousReusableBufferProcessor<>(10000, 500);
|
||||
mBufferProcessor.setListener(new Listener<List<ReusableAudioPacket>>()
|
||||
{
|
||||
@Override
|
||||
public void receive(List<ReusableAudioPacket> reusableAudioPackets)
|
||||
{
|
||||
for(ReusableAudioPacket audioPacket: reusableAudioPackets)
|
||||
{
|
||||
mAudioMetadataProcessor.process(audioPacket);
|
||||
mBroadcaster.broadcast(audioPacket);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void start()
|
||||
{
|
||||
mLog.info("Audio packet manager started");
|
||||
mBufferProcessor.start();
|
||||
}
|
||||
|
||||
public void stop()
|
||||
{
|
||||
mLog.info("Audio packet manager stopped");
|
||||
mBufferProcessor.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary processing method for receiving audio packets from all processing channels.
|
||||
*/
|
||||
@Override
|
||||
public void receive(ReusableAudioPacket audioPacket)
|
||||
{
|
||||
mBufferProcessor.receive(audioPacket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the listener to receive enriched audio packets from this manager
|
||||
*/
|
||||
public void addListener(Listener<ReusableAudioPacket> listener)
|
||||
{
|
||||
mBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the listener from receiving enriched audio packets from this manager
|
||||
*/
|
||||
public void removeListener(Listener<ReusableAudioPacket> listener)
|
||||
{
|
||||
mBroadcaster.removeListener(listener);
|
||||
}
|
||||
}
|
||||
436
src/main/java/io/github/dsheirer/audio/AudioSegment.java
Normal file
436
src/main/java/io/github/dsheirer/audio/AudioSegment.java
Normal file
@@ -0,0 +1,436 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Audio segment containing all related metadata and a dynamic collection of audio packets. An audio segment can be
|
||||
* a discrete (ie start/stop) audio event, or it may be a time-constrained portion of an ongoing continuous audio
|
||||
* broadcast. Since the audio segment is held in memory until all consumers have finished processing the segment,
|
||||
* producers should constrain the duration of each audio segment to a reasonable duration.
|
||||
*
|
||||
* Producers can link time-constrained audio segments from a continous broadcast (e.g. FM radio station) so that
|
||||
* consumers can identify audio segments that belong to a continous stream. This linkage presents a memory leak
|
||||
* potential. Therefore, the audio segment uses the consumer counter to trigger an unlinking via the dispose() mehtod.
|
||||
* Accurate accounting of consumer count via the increment/decrementConsumerCount() methods is essential for good
|
||||
* memory management.
|
||||
*
|
||||
* 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
|
||||
* the segment is complete.
|
||||
*/
|
||||
public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
|
||||
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
|
||||
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
||||
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
|
||||
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
|
||||
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
|
||||
private List<float[]> mAudioBuffers = new CopyOnWriteArrayList();
|
||||
private AtomicInteger mConsumerCount = new AtomicInteger();
|
||||
private AliasList mAliasList;
|
||||
private long mStartTimestamp = System.currentTimeMillis();
|
||||
private boolean mDisposing = false;
|
||||
private AudioSegment mLinkedAudioSegment;
|
||||
private int mTimeslot;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param aliasList for accessing aliases associated with identifiers for this audio segment.
|
||||
*/
|
||||
public AudioSegment(AliasList aliasList, int timeslot)
|
||||
{
|
||||
mAliasList = aliasList;
|
||||
mTimeslot = timeslot;
|
||||
mIdentifierCollection.setTimeslot(timeslot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Timeslot for this audio segment
|
||||
*/
|
||||
public int getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creation timestamp for this audio segment
|
||||
* @return long milliseconds since epoch
|
||||
*/
|
||||
public long getStartTimestamp()
|
||||
{
|
||||
return mStartTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete property is used by the audio segment producer to signal that the segment is complete and no
|
||||
* additional audio or identifiers will be added to the segment.
|
||||
*
|
||||
* @return complete property
|
||||
*/
|
||||
public BooleanProperty completeProperty()
|
||||
{
|
||||
return mComplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable list of broadcast channels for this audio segment. Broadcast channels are added to this segment
|
||||
* across the life-cycle of the segment. As each new alias identifier is added to the segment, any broadcast
|
||||
* channels assigned to the alias are added to this list. The audio segment producer can also add broadcast
|
||||
* channels to this list.
|
||||
*
|
||||
* @return observable set of broadcast channels
|
||||
*/
|
||||
public ObservableSet<BroadcastChannel> broadcastChannelsProperty()
|
||||
{
|
||||
return mBroadcastChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of broadcast channels from identifier associated aliases for this segment.
|
||||
*/
|
||||
public Set<BroadcastChannel> getBroadcastChannels()
|
||||
{
|
||||
return Collections.unmodifiableSet(mBroadcastChannels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this segment has audio streaming broadcast channels specified.
|
||||
*/
|
||||
public boolean hasBroadcastChannels()
|
||||
{
|
||||
return !mBroadcastChannels.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Property to signal that this audio segment should be recorded. This property can either be set by the producer
|
||||
* of the audio segment, or it can be flipped to true by any aliases that are added to this segment that require
|
||||
* audio associated with the identifier to be recorded.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public BooleanProperty recordAudioProperty()
|
||||
{
|
||||
return mRecordAudio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio playback/monitor priority specified by identifier associated aliases for this segment.
|
||||
*/
|
||||
public IntegerProperty monitorPriorityProperty()
|
||||
{
|
||||
return mMonitorPriority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if at least one of the identifier associated aliases for this segments specifies Do Not Monitor.
|
||||
*/
|
||||
public boolean isDoNotMonitor()
|
||||
{
|
||||
return mMonitorPriority.get() <= Priority.DO_NOT_MONITOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias list for this audio segment
|
||||
*/
|
||||
public AliasList getAliasList()
|
||||
{
|
||||
return mAliasList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment is linked to a preceding audio segment.
|
||||
*/
|
||||
public boolean isLinked()
|
||||
{
|
||||
return mLinkedAudioSegment != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment is linked to the argument audio segment
|
||||
* @param audioSegment to check for linkage
|
||||
* @return true if this segment is linked to the argument
|
||||
*/
|
||||
public boolean isLinkedTo(AudioSegment audioSegment)
|
||||
{
|
||||
return isLinked() && audioSegment != null && mLinkedAudioSegment.equals(audioSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional linked audio segment is the audio segment that precedes this audio segment in a continuous audio
|
||||
* broadcast.
|
||||
*
|
||||
* @return linked audio segment or null.
|
||||
*/
|
||||
public AudioSegment getLinkedAudioSegment()
|
||||
{
|
||||
return mLinkedAudioSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links this audio segment to a preceeding/previous audio segment
|
||||
* @param previousAudioSegment to set as the predecessor
|
||||
*/
|
||||
public void linkTo(AudioSegment previousAudioSegment)
|
||||
{
|
||||
mLinkedAudioSegment = previousAudioSegment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable identifier collection for this audio segment
|
||||
*/
|
||||
public IdentifierCollection getIdentifierCollection()
|
||||
{
|
||||
return mIdentifierCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the collection of identifiers to this segment's identifier collection
|
||||
* @param identifiers to pre-load into this audio segment
|
||||
*/
|
||||
void addIdentifiers(Collection<Identifier> identifiers)
|
||||
{
|
||||
for(Identifier identifier: identifiers)
|
||||
{
|
||||
addIdentifier(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmodifiable copy of the list of audio buffers for this segment.
|
||||
*
|
||||
* @return list of audio buffers
|
||||
*/
|
||||
public List<float[]> getAudioBuffers()
|
||||
{
|
||||
return Collections.unmodifiableList(mAudioBuffers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count of audio buffers contained in this segment.
|
||||
*
|
||||
* Note: audio buffers can be added to an audio segment throughout the segment's life-cycle by the audio producer.
|
||||
*/
|
||||
public int getAudioBufferCount()
|
||||
{
|
||||
return mAudioBuffers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the audio buffer at the specified index
|
||||
* @param index of the buffer to fetch
|
||||
* @return audio buffer
|
||||
* @throws IllegalArgumentException if requested index is not valid
|
||||
*/
|
||||
public float[] getAudioBuffer(int index)
|
||||
{
|
||||
if(0 <= index && index < getAudioBufferCount())
|
||||
{
|
||||
return mAudioBuffers.get(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("Requested audio buffer at index [" + index + "] does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment has one or more audio buffers
|
||||
*/
|
||||
public boolean hasAudio()
|
||||
{
|
||||
return !mAudioBuffers.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all audio buffers and decrements the user count on each so that the audio buffer can be reclaimed.
|
||||
*/
|
||||
private void dispose()
|
||||
{
|
||||
mDisposing = true;
|
||||
mAudioBuffers.clear();
|
||||
mIdentifierCollection.clear();
|
||||
mIdentifierUpdateNotificationBroadcaster.clear();
|
||||
mLinkedAudioSegment = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the consumer count to indicate that a consumer is currently processing this segment. When the
|
||||
* consumer count returns to zero, this indicates that all consumers are finished with the audio segment and the
|
||||
* resources can be reclaimed.
|
||||
*
|
||||
* Consumer count should only be increased by the producer of the audio segment, or if a consumer distributes the
|
||||
* segment to additional consumers.
|
||||
*/
|
||||
public void incrementConsumerCount()
|
||||
{
|
||||
mConsumerCount.incrementAndGet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements the consumer count. Consumers of this audio segment should invoke this method to signal that they
|
||||
* will no longer need this audio segment. When all consumers are finished with an audio segment, the audio
|
||||
* segment resources will be reclaimed.
|
||||
*/
|
||||
public void decrementConsumerCount()
|
||||
{
|
||||
int count = mConsumerCount.decrementAndGet();
|
||||
|
||||
if(count <= 0)
|
||||
{
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an audio buffer to this segment. Note the producer of the audio buffer should increment the user count
|
||||
* of the buffer prior to adding it to this segment. This segment will decrement the audio buffer user count once
|
||||
* all consumers of this audio segment have de-registered via the decrementConsumerCount() method.
|
||||
*
|
||||
* @param audioBuffer to add to this segment
|
||||
*/
|
||||
public void addAudio(float[] audioBuffer)
|
||||
{
|
||||
if(audioBuffer == null)
|
||||
{
|
||||
throw new IllegalArgumentException("Can't add null audio buffer");
|
||||
}
|
||||
|
||||
if(mDisposing)
|
||||
{
|
||||
throw new IllegalStateException("Can't add audio to an audio segment that is being disposed");
|
||||
}
|
||||
|
||||
mAudioBuffers.add(audioBuffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener to receive identifier update notifications
|
||||
*/
|
||||
public void addIdentifierUpdateNotificationListener(Listener<IdentifierUpdateNotification> listener)
|
||||
{
|
||||
mIdentifierUpdateNotificationBroadcaster.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the identifier update listener.
|
||||
*/
|
||||
public void removeIdentifierUpdateNotificationListener(Listener<IdentifierUpdateNotification> listener)
|
||||
{
|
||||
mIdentifierUpdateNotificationBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the identifier to this audio segment and updates record, priority and streaming properties.
|
||||
*
|
||||
* Note: identifiers pass to this method must be checked for timeslot match.
|
||||
*/
|
||||
public void addIdentifier(Identifier identifier)
|
||||
{
|
||||
mIdentifierCollection.update(identifier);
|
||||
|
||||
List<Alias> aliases = mAliasList.getAliases(identifier);
|
||||
|
||||
for(Alias alias: aliases)
|
||||
{
|
||||
if(alias.isRecordable())
|
||||
{
|
||||
mRecordAudio.set(true);
|
||||
}
|
||||
|
||||
//Add all broadcast channels for the alias ... let the set handle duplication.
|
||||
for(BroadcastChannel broadcastChannel: alias.getBroadcastChannels())
|
||||
{
|
||||
mBroadcastChannels.add(broadcastChannel);
|
||||
}
|
||||
|
||||
//Only assign a playback priority if it is lower priority than the current setting.
|
||||
int playbackPriority = alias.getPlaybackPriority();
|
||||
|
||||
if(playbackPriority < mMonitorPriority.get())
|
||||
{
|
||||
mMonitorPriority.set(playbackPriority);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes identifier update notifications and updates audio segment properties using the associated aliases
|
||||
* from the alias list.
|
||||
*
|
||||
* @param identifierUpdateNotification containing an identifier.
|
||||
*/
|
||||
@Override
|
||||
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
|
||||
{
|
||||
//Only process add updates that match this timeslot
|
||||
if(identifierUpdateNotification.getTimeslot() == getTimeslot())
|
||||
{
|
||||
if(identifierUpdateNotification.isAdd() || identifierUpdateNotification.isSilentAdd())
|
||||
{
|
||||
addIdentifier(identifierUpdateNotification.getIdentifier());
|
||||
}
|
||||
|
||||
mIdentifierUpdateNotificationBroadcaster.broadcast(identifierUpdateNotification);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Audio Segment\n Timeslot:").append(getTimeslot()).append("\n");
|
||||
sb.append(" Start Time:").append(new Date(getStartTimestamp()));
|
||||
sb.append(" Linked:").append(isLinked()).append("\n");
|
||||
sb.append(" Recordable:").append(recordAudioProperty().get()).append("\n");
|
||||
sb.append("Do Not Monitor:").append(isDoNotMonitor()).append("\n");
|
||||
sb.append(" Stream To:").append(getBroadcastChannels()).append("\n");
|
||||
sb.append(" Identifiers:\n").append(getIdentifierCollection()).append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.AbstractReusableBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AudioSegmentBroadcaster<T extends AudioSegment> extends Broadcaster<T>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioSegmentBroadcaster.class);
|
||||
|
||||
/**
|
||||
* Increments the consumer count for the reusable complex buffer and then broadcasts the buffer to all registered
|
||||
* listeners.
|
||||
*
|
||||
* The total consumer count is established and applied to the buffer prior to dispatching. If we were to simply
|
||||
* increment the consumer count prior to sending to each consumer, there is a possibility that the consumer could
|
||||
* immediately decrement the consumer count and prematurely signal that the buffer is ready for disposal before we
|
||||
* send the buffer to all consumers.
|
||||
*/
|
||||
@Override
|
||||
public void broadcast(T audioSegment)
|
||||
{
|
||||
for(Listener<T> listener : mListeners)
|
||||
{
|
||||
audioSegment.incrementConsumerCount();
|
||||
listener.receive(audioSegment);
|
||||
}
|
||||
|
||||
//Decrement consumer counter for this broadcaster
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.ConversionUtils;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -32,18 +31,17 @@ public class AudioUtils
|
||||
/**
|
||||
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
|
||||
*/
|
||||
public static byte[] convertTo16BitSamples(List<ReusableAudioPacket> audioPackets)
|
||||
public static byte[] convertTo16BitSamples(List<float[]> buffers)
|
||||
{
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
for(ReusableAudioPacket audioPacket: audioPackets)
|
||||
for(float[] audioBuffer: buffers)
|
||||
{
|
||||
//Converting from 32-bit floats to signed 16-bit samples
|
||||
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples());
|
||||
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioBuffer);
|
||||
stream.write(buffer.array());
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
}
|
||||
catch(IOException e)
|
||||
@@ -53,4 +51,29 @@ public class AudioUtils
|
||||
|
||||
return stream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
|
||||
*/
|
||||
public static byte[] convert(List<float[]> audioBuffers)
|
||||
{
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
for(float[] audioBuffer: audioBuffers)
|
||||
{
|
||||
//Converting from 32-bit floats to signed 16-bit samples
|
||||
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioBuffer);
|
||||
stream.write(buffer.array());
|
||||
}
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("Error writing converted PCM bytes to output stream");
|
||||
}
|
||||
|
||||
return stream.toByteArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
|
||||
public interface IAudioPacketListener
|
||||
{
|
||||
public Listener<ReusableAudioPacket> getAudioPacketListener();
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
|
||||
public interface IAudioPacketProvider
|
||||
{
|
||||
public void setAudioPacketListener( Listener<ReusableAudioPacket> listener );
|
||||
public void removeAudioPacketListener();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Interface for audio segment listener
|
||||
*/
|
||||
public interface IAudioSegmentListener
|
||||
{
|
||||
/**
|
||||
* Get the audio segment listener
|
||||
*/
|
||||
Listener<AudioSegment> getAudioSegmentListener();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Interface for audio segment provider
|
||||
*/
|
||||
public interface IAudioSegmentProvider
|
||||
{
|
||||
/**
|
||||
* Registers the audio segment listener
|
||||
* @param listener of audio segments
|
||||
*/
|
||||
void setAudioSegmentListener(Listener<AudioSegment> listener);
|
||||
|
||||
/**
|
||||
* De-registers the audio segment listener
|
||||
*/
|
||||
void removeAudioSegmentListener();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -204,6 +204,10 @@ public abstract class AudioBroadcaster implements Listener<AudioRecording>
|
||||
mAudioRecordingQueue.offer(recording);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
else
|
||||
{
|
||||
recording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,22 +18,25 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class AudioRecording implements Comparable<AudioRecording>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioRecording.class);
|
||||
|
||||
private Path mPath;
|
||||
private long mStartTime;
|
||||
private long mRecordingLength;
|
||||
private AtomicInteger mPendingReplayCount = new AtomicInteger();
|
||||
private IdentifierCollection mIdentifierCollection;
|
||||
private Collection<BroadcastChannel> mBroadcastChannels;
|
||||
|
||||
/**
|
||||
* Audio recording that is ready to be streamed
|
||||
@@ -43,9 +46,11 @@ public class AudioRecording implements Comparable<AudioRecording>
|
||||
* @param start time of recording in milliseconds since epoch
|
||||
* @param recordingLength in milliseconds
|
||||
*/
|
||||
public AudioRecording(Path path, IdentifierCollection identifierCollection, long start, long recordingLength)
|
||||
public AudioRecording(Path path, Collection<BroadcastChannel> broadcastChannels,
|
||||
IdentifierCollection identifierCollection, long start, long recordingLength)
|
||||
{
|
||||
mPath = path;
|
||||
mBroadcastChannels = broadcastChannels;
|
||||
mIdentifierCollection = identifierCollection;
|
||||
mStartTime = start;
|
||||
mRecordingLength = recordingLength;
|
||||
@@ -59,6 +64,14 @@ public class AudioRecording implements Comparable<AudioRecording>
|
||||
return mPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of broadcast channels that this recording should be streamed to
|
||||
*/
|
||||
public Collection<BroadcastChannel> getBroadcastChannels()
|
||||
{
|
||||
return mBroadcastChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional audio metadata/identifiers for the recording.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioSegmentRecorder;
|
||||
import io.github.dsheirer.record.RecordFormat;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on
|
||||
* disk and enqueues the temporary recording for streaming.
|
||||
*/
|
||||
public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingManager.class);
|
||||
private LinkedTransferQueue<AudioSegment> mNewAudioSegments = new LinkedTransferQueue<>();
|
||||
private List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private Listener<AudioRecording> mAudioRecordingListener;
|
||||
private BroadcastFormat mBroadcastFormat;
|
||||
private UserPreferences mUserPreferences;
|
||||
private ScheduledFuture<?> mAudioSegmentProcessorFuture;
|
||||
private int mNextRecordingNumber = 1;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param listener to receive completed audio recordings
|
||||
* @param broadcastFormat for temporary recordings
|
||||
* @param userPreferences to manage recording directories
|
||||
*/
|
||||
public AudioStreamingManager(Listener<AudioRecording> listener, BroadcastFormat broadcastFormat, UserPreferences userPreferences)
|
||||
{
|
||||
mAudioRecordingListener = listener;
|
||||
mBroadcastFormat = broadcastFormat;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary receive method
|
||||
*/
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
mNewAudioSegments.add(audioSegment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the scheduled audio segment processor
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mAudioSegmentProcessorFuture == null)
|
||||
{
|
||||
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 250, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the scheduled audio segment processor
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mAudioSegmentProcessorFuture != null)
|
||||
{
|
||||
mAudioSegmentProcessorFuture.cancel(true);
|
||||
mAudioSegmentProcessorFuture = null;
|
||||
}
|
||||
|
||||
for(AudioSegment audioSegment: mNewAudioSegments)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
|
||||
mNewAudioSegments.clear();
|
||||
|
||||
for(AudioSegment audioSegment: mAudioSegments)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
|
||||
mAudioSegments.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Main processing method to process audio segments
|
||||
*/
|
||||
private void processAudioSegments()
|
||||
{
|
||||
mNewAudioSegments.drainTo(mAudioSegments);
|
||||
|
||||
Iterator<AudioSegment> it = mAudioSegments.iterator();
|
||||
AudioSegment audioSegment;
|
||||
while(it.hasNext())
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.completeProperty().get())
|
||||
{
|
||||
it.remove();
|
||||
|
||||
if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
|
||||
{
|
||||
Path path = getTemporaryRecordingPath();
|
||||
long length = 0;
|
||||
|
||||
for(float[] audioBuffer: audioSegment.getAudioBuffers())
|
||||
{
|
||||
length += audioBuffer.length;
|
||||
}
|
||||
|
||||
length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3);
|
||||
IdentifierCollection identifierCollectionCopy =
|
||||
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(),
|
||||
identifierCollectionCopy, audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error recording temporary stream MP3");
|
||||
}
|
||||
}
|
||||
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary streaming recording file path
|
||||
*/
|
||||
private Path getTemporaryRecordingPath()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
|
||||
|
||||
//Check for integer overflow and readjust negative value to 0
|
||||
if(mNextRecordingNumber < 0)
|
||||
{
|
||||
mNextRecordingNumber = 1;
|
||||
}
|
||||
|
||||
int recordingNumber = mNextRecordingNumber++;
|
||||
|
||||
sb.append(recordingNumber).append("_");
|
||||
sb.append(TimeStamp.getLongTimeStamp("_"));
|
||||
sb.append(mBroadcastFormat.getFileExtension());
|
||||
|
||||
Path temporaryRecordingPath = mUserPreferences.getDirectoryPreference().getDirectoryStreaming().resolve(sb.toString());
|
||||
|
||||
return temporaryRecordingPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled runnable to process audio segments.
|
||||
*/
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
processAudioSegments();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error processing audio segments for streaming", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,13 +44,9 @@ import io.github.dsheirer.gui.editor.Editor;
|
||||
import io.github.dsheirer.gui.editor.EmptyEditor;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioRecorder;
|
||||
import io.github.dsheirer.record.mp3.MP3Recorder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class BroadcastFactory
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastFactory.class);
|
||||
@@ -186,21 +182,6 @@ public class BroadcastFactory
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
|
||||
*/
|
||||
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
|
||||
{
|
||||
switch(broadcastFormat)
|
||||
{
|
||||
case MP3:
|
||||
return new MP3Recorder(path);
|
||||
default:
|
||||
mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format)
|
||||
{
|
||||
switch(format)
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
@@ -29,7 +28,6 @@ import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -51,7 +49,7 @@ import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class BroadcastModel extends AbstractTableModel implements Listener<ReusableAudioPacket>
|
||||
public class BroadcastModel extends AbstractTableModel implements Listener<AudioRecording>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
|
||||
|
||||
@@ -76,7 +74,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
|
||||
private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
|
||||
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
||||
private IconManager mIconManager;
|
||||
private StreamManager mStreamManager;
|
||||
private AliasModel mAliasModel;
|
||||
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
|
||||
|
||||
@@ -87,8 +84,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
mIconManager = iconManager;
|
||||
mStreamManager = new StreamManager(new CompletedRecordingListener(), BroadcastFormat.MP3, userPreferences);
|
||||
mStreamManager.start();
|
||||
|
||||
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
|
||||
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS);
|
||||
@@ -281,22 +276,28 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ReusableAudioPacket audioPacket)
|
||||
public void receive(AudioRecording audioRecording)
|
||||
{
|
||||
if(audioPacket.isStreamable())
|
||||
if(audioRecording != null && !audioRecording.getBroadcastChannels().isEmpty())
|
||||
{
|
||||
for(BroadcastChannel channel: audioPacket.getBroadcastChannels())
|
||||
for(BroadcastChannel broadcastChannel : audioRecording.getBroadcastChannels())
|
||||
{
|
||||
if(mBroadcasterMap.containsKey(channel.getChannelName()))
|
||||
String channelName = broadcastChannel.getChannelName();
|
||||
|
||||
if(channelName != null)
|
||||
{
|
||||
audioPacket.incrementUserCount();
|
||||
mStreamManager.receive(audioPacket);
|
||||
return;
|
||||
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
audioRecording.addPendingReplay();
|
||||
audioBroadcaster.receive(audioRecording);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioPacket.decrementUserCount();
|
||||
mRecordingQueue.add(audioRecording);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -311,19 +312,9 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
|
||||
{
|
||||
@Override
|
||||
public void receive(BroadcastEvent broadcastEvent)
|
||||
{
|
||||
process(broadcastEvent);
|
||||
}
|
||||
});
|
||||
|
||||
audioBroadcaster.setListener(broadcastEvent -> process(broadcastEvent));
|
||||
audioBroadcaster.start();
|
||||
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
|
||||
|
||||
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||
|
||||
if(index >= 0)
|
||||
@@ -710,43 +701,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes completed audio recordings and distributes them to the audio broadcasters. Adds the recording to
|
||||
* the audio recording queue to be monitored for deletion.
|
||||
*/
|
||||
public class CompletedRecordingListener implements Listener<AudioRecording>
|
||||
{
|
||||
@Override
|
||||
public void receive(AudioRecording audioRecording)
|
||||
{
|
||||
if(audioRecording != null && audioRecording.hasIdentifierCollection())
|
||||
{
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
for(BroadcastChannel broadcastChannel : aliasList.getBroadcastChannels(audioRecording.getIdentifierCollection()))
|
||||
{
|
||||
String channelName = broadcastChannel.getChannelName();
|
||||
|
||||
if(channelName != null)
|
||||
{
|
||||
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
audioRecording.addPendingReplay();
|
||||
audioBroadcaster.receive(audioRecording);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mRecordingQueue.add(audioRecording);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the recording queue and removes any recordings that have no pending replays by audio broadcasters
|
||||
*/
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioRecorder;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class StreamManager implements Listener<ReusableAudioPacket>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
|
||||
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
|
||||
|
||||
private static AtomicInteger sNextRecordingNumber = new AtomicInteger();
|
||||
|
||||
private Listener<AudioRecording> mAudioRecordingListener;
|
||||
private BroadcastFormat mBroadcastFormat;
|
||||
private UserPreferences mUserPreferences;
|
||||
private Map<Integer,AudioRecorder> mStreamRecorders = new HashMap<>();
|
||||
private Runnable mRecorderMonitor;
|
||||
private ScheduledFuture<?> mRecorderMonitorFuture;
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
* Stream manager processes all incoming audio packets and reassembles individual audio streams, converts audio
|
||||
* to desired output format and persists each stream to disc. Each recording is capped at a maximum length to
|
||||
* ensure that recordings don't run too long before they are streamed out and to ensure that inactive recordings
|
||||
* are closed in a timely fashion.
|
||||
*
|
||||
* Completed streamable audio recordings are nominated to the output listener (for broadcast) upon completion
|
||||
*
|
||||
* @param listener to receive completed audio recordings
|
||||
* @param broadcastFormat for the stream
|
||||
* @param userPreferences to obtain the temporary streaming recording directory
|
||||
*/
|
||||
public StreamManager(Listener<AudioRecording> listener, BroadcastFormat broadcastFormat, UserPreferences userPreferences)
|
||||
{
|
||||
mAudioRecordingListener = listener;
|
||||
mBroadcastFormat = broadcastFormat;
|
||||
mUserPreferences = userPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the stream manager. Schedules a processor to monitor for inactive temporary stream recorders
|
||||
* every 2 seconds.
|
||||
*/
|
||||
public void start()
|
||||
{
|
||||
if(mRunning.compareAndSet(false, true))
|
||||
{
|
||||
if(mRecorderMonitor == null)
|
||||
{
|
||||
mRecorderMonitor = new RecorderMonitor();
|
||||
}
|
||||
|
||||
mRecorderMonitorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mRecorderMonitor,
|
||||
0,2, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the stream manager. Stops the inactive temporary stream recorder monitoring thread.
|
||||
*/
|
||||
public void stop()
|
||||
{
|
||||
if(mRunning.compareAndSet(true, false))
|
||||
{
|
||||
if(mRecorderMonitorFuture != null)
|
||||
{
|
||||
mRecorderMonitorFuture.cancel(true);
|
||||
}
|
||||
|
||||
synchronized(mStreamRecorders)
|
||||
{
|
||||
List<Integer> streamKeys = new ArrayList<>(mStreamRecorders.keySet());
|
||||
|
||||
for(Integer streamKey : streamKeys)
|
||||
{
|
||||
removeRecorder(streamKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ReusableAudioPacket audioPacket)
|
||||
{
|
||||
if(mRunning.get() && audioPacket.hasIdentifierCollection())
|
||||
{
|
||||
synchronized(mStreamRecorders)
|
||||
{
|
||||
int channelMetadataID = audioPacket.getAudioChannelId();
|
||||
|
||||
ReusableAudioPacket.Type type = audioPacket.getType();
|
||||
|
||||
if(type == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
if(mStreamRecorders.containsKey(channelMetadataID))
|
||||
{
|
||||
AudioRecorder recorder = mStreamRecorders.get(channelMetadataID);
|
||||
|
||||
if(recorder != null)
|
||||
{
|
||||
audioPacket.incrementUserCount();
|
||||
recorder.receive(audioPacket);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(), mBroadcastFormat);
|
||||
recorder.start();
|
||||
audioPacket.incrementUserCount();
|
||||
recorder.receive(audioPacket);
|
||||
mStreamRecorders.put(channelMetadataID, recorder);
|
||||
}
|
||||
}
|
||||
else if(type == ReusableAudioPacket.Type.END)
|
||||
{
|
||||
removeRecorder(channelMetadataID);
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("Unrecognized Audio Packet Type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the recorder associated with the source channel ID.
|
||||
*
|
||||
* Note: this method invocation is not thread safe and must be invoked by a thread safe mechanism that protects the
|
||||
* mStreamRecorders map.
|
||||
*
|
||||
* @param sourceChannelID identifying the recorder
|
||||
*/
|
||||
private void removeRecorder(Integer sourceChannelID)
|
||||
{
|
||||
if(mStreamRecorders.containsKey(sourceChannelID))
|
||||
{
|
||||
AudioRecorder recorder = mStreamRecorders.remove(sourceChannelID);
|
||||
|
||||
recorder.close(new Listener<AudioRecorder>()
|
||||
{
|
||||
@Override
|
||||
public void receive(AudioRecorder audioRecorder)
|
||||
{
|
||||
AudioRecording audioRecording =
|
||||
new AudioRecording(audioRecorder.getPath(), audioRecorder.getIdentifierCollection(),
|
||||
audioRecorder.getTimeRecordingStart(), audioRecorder.getRecordingLength());
|
||||
|
||||
if(mAudioRecordingListener != null)
|
||||
{
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary streaming recording file path
|
||||
*/
|
||||
private Path getTemporaryRecordingPath()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(BroadcastModel.TEMPORARY_STREAM_FILE_SUFFIX);
|
||||
|
||||
int recordingNumber = sNextRecordingNumber.incrementAndGet();
|
||||
|
||||
//Check for integer overflow and readjust negative value to 0
|
||||
if(recordingNumber < 0)
|
||||
{
|
||||
sNextRecordingNumber.set(0);
|
||||
recordingNumber = sNextRecordingNumber.incrementAndGet();
|
||||
}
|
||||
sb.append(recordingNumber).append("_");
|
||||
sb.append(TimeStamp.getLongTimeStamp("_"));
|
||||
sb.append(mBroadcastFormat.getFileExtension());
|
||||
|
||||
Path temporaryRecordingPath = mUserPreferences.getDirectoryPreference().getDirectoryStreaming().resolve(sb.toString());
|
||||
|
||||
return temporaryRecordingPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors recorders to ensure they don't exceed the maximum life-span allowed for a recording.
|
||||
*/
|
||||
public class RecorderMonitor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
synchronized(mStreamRecorders)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
mStreamRecorders.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getTimeRecordingStart() + MAXIMUM_RECORDER_LIFESPAN_MILLIS < now)
|
||||
.forEach(entry ->
|
||||
{
|
||||
mLog.info("cycling recorder - max temporary streaming recording time limit reached [" +
|
||||
entry.getValue().getPath().toString() + "]");
|
||||
|
||||
removeRecorder(entry.getKey());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -178,6 +178,15 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
|
||||
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
|
||||
}
|
||||
|
||||
if(to == null)
|
||||
{
|
||||
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
|
||||
if(!toIdentifiers.isEmpty())
|
||||
{
|
||||
to = toIdentifiers.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
@@ -194,7 +203,17 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
|
||||
sb.append("TO:UNKNOWN");
|
||||
}
|
||||
|
||||
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.FROM);
|
||||
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
|
||||
if(from == null)
|
||||
{
|
||||
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
|
||||
|
||||
if(!fromIdentifiers.isEmpty())
|
||||
{
|
||||
from = fromIdentifiers.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
if(from != null)
|
||||
{
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
|
||||
|
||||
@@ -203,6 +205,15 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
|
||||
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
|
||||
}
|
||||
|
||||
if(to == null)
|
||||
{
|
||||
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
|
||||
if(!toIdentifiers.isEmpty())
|
||||
{
|
||||
to = toIdentifiers.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sb.append("TO:").append(to);
|
||||
@@ -219,7 +230,17 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
|
||||
sb.append("TO:UNKNOWN");
|
||||
}
|
||||
|
||||
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.FROM);
|
||||
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
|
||||
if(from == null)
|
||||
{
|
||||
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
|
||||
|
||||
if(!fromIdentifiers.isEmpty())
|
||||
{
|
||||
from = fromIdentifiers.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
if(from != null)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -291,6 +291,16 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
||||
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
|
||||
}
|
||||
|
||||
if(to == null)
|
||||
{
|
||||
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
|
||||
|
||||
if(!toIdentifiers.isEmpty())
|
||||
{
|
||||
to = toIdentifiers.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
if(to != null)
|
||||
{
|
||||
sbTitle2.append("TO:").append(to);
|
||||
@@ -307,7 +317,17 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
||||
sbTitle2.append("TO:UNKNOWN");
|
||||
}
|
||||
|
||||
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.FROM);
|
||||
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
|
||||
|
||||
if(from == null)
|
||||
{
|
||||
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
|
||||
|
||||
if(!fromIdentifiers.isEmpty())
|
||||
{
|
||||
from = fromIdentifiers.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
if(from != null)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -32,9 +33,9 @@ public abstract class AmbeAudioModule extends JmbeAudioModule
|
||||
private static final String AMBE_CODEC = "AMBE 3600 x 2450";
|
||||
private static boolean sLibraryStatusLogged = false;
|
||||
|
||||
public AmbeAudioModule(UserPreferences userPreferences)
|
||||
public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
|
||||
{
|
||||
super(userPreferences);
|
||||
super(userPreferences, aliasList);
|
||||
|
||||
if(!sLibraryStatusLogged)
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -32,9 +33,9 @@ public abstract class ImbeAudioModule extends JmbeAudioModule
|
||||
private static final String IMBE_CODEC = "IMBE";
|
||||
private static boolean sLibraryStatusLogged = false;
|
||||
|
||||
public ImbeAudioModule(UserPreferences userPreferences)
|
||||
public ImbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
|
||||
{
|
||||
super(userPreferences);
|
||||
super(userPreferences, aliasList);
|
||||
|
||||
if(!sLibraryStatusLogged)
|
||||
{
|
||||
|
||||
@@ -23,14 +23,15 @@
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.audio.AbstractAudioModule;
|
||||
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.IMessageListener;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
||||
import jmbe.iface.IAudioCodec;
|
||||
import jmbe.iface.IAudioCodecLibrary;
|
||||
import org.slf4j.Logger;
|
||||
@@ -44,17 +45,18 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener
|
||||
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
|
||||
ISquelchStateListener
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
|
||||
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
|
||||
private static List<String> mLibraryLoadStatusLogged = new ArrayList<>();
|
||||
private IAudioCodec mAudioCodec;
|
||||
private UserPreferences mUserPreferences;
|
||||
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("JmbeAudioModule");
|
||||
|
||||
public JmbeAudioModule(UserPreferences userPreferences)
|
||||
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
|
||||
{
|
||||
super(aliasList);
|
||||
mUserPreferences = userPreferences;
|
||||
MyEventBus.getEventBus().register(this);
|
||||
loadConverter();
|
||||
@@ -73,11 +75,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
return getAudioCodec() != null;
|
||||
}
|
||||
|
||||
protected ReusableAudioPacketQueue getAudioPacketQueue()
|
||||
{
|
||||
return mAudioPacketQueue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Listener<IMessage> getMessageListener()
|
||||
{
|
||||
|
||||
@@ -22,21 +22,9 @@
|
||||
|
||||
package io.github.dsheirer.audio.codec.mbe;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.github.dsheirer.audio.convert.thumbdv.ThumbDv;
|
||||
import io.github.dsheirer.audio.convert.thumbdv.message.response.AmbeResponse;
|
||||
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
|
||||
import io.github.dsheirer.record.wave.AudioPacketWaveRecorder;
|
||||
import io.github.dsheirer.record.wave.WaveMetadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Utility for converting MBE call sequences (*.mbe) to PCM wave audio format
|
||||
*/
|
||||
@@ -44,64 +32,64 @@ public class MBECallSequenceConverter
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceConverter.class);
|
||||
|
||||
public static void convert(Path input, Path output) throws IOException
|
||||
{
|
||||
InputStream inputStream = Files.newInputStream(input);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
|
||||
convert(sequence, output);
|
||||
}
|
||||
|
||||
public static void convert(MBECallSequence callSequence, Path outputPath)
|
||||
{
|
||||
if(callSequence == null || callSequence.isEncrypted())
|
||||
{
|
||||
throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
|
||||
}
|
||||
|
||||
if(callSequence != null && !callSequence.isEncrypted())
|
||||
{
|
||||
ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
|
||||
|
||||
AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
|
||||
recorder.start();
|
||||
|
||||
long delayMillis = 0;
|
||||
|
||||
try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
|
||||
{
|
||||
thumbDv.start();
|
||||
for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
|
||||
{
|
||||
mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
|
||||
thumbDv.decode(voiceFrame.getFrameBytes());
|
||||
delayMillis += 30;
|
||||
}
|
||||
|
||||
if(delayMillis > 0)
|
||||
{
|
||||
delayMillis += 1000;
|
||||
try
|
||||
{
|
||||
Thread.sleep(delayMillis);
|
||||
}
|
||||
catch(InterruptedException ie)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error", ioe);
|
||||
}
|
||||
|
||||
recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
// public static void convert(Path input, Path output) throws IOException
|
||||
// {
|
||||
// InputStream inputStream = Files.newInputStream(input);
|
||||
// ObjectMapper mapper = new ObjectMapper();
|
||||
// MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
|
||||
// convert(sequence, output);
|
||||
// }
|
||||
//
|
||||
// public static void convert(MBECallSequence callSequence, Path outputPath)
|
||||
// {
|
||||
// if(callSequence == null || callSequence.isEncrypted())
|
||||
// {
|
||||
// throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
|
||||
// }
|
||||
//
|
||||
// if(callSequence != null && !callSequence.isEncrypted())
|
||||
// {
|
||||
// ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
|
||||
//
|
||||
// AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
|
||||
// recorder.start();
|
||||
//
|
||||
// long delayMillis = 0;
|
||||
//
|
||||
// try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
|
||||
// {
|
||||
// thumbDv.start();
|
||||
// for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
|
||||
// {
|
||||
// mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
|
||||
// thumbDv.decode(voiceFrame.getFrameBytes());
|
||||
// delayMillis += 30;
|
||||
// }
|
||||
//
|
||||
// if(delayMillis > 0)
|
||||
// {
|
||||
// delayMillis += 1000;
|
||||
// try
|
||||
// {
|
||||
// Thread.sleep(delayMillis);
|
||||
// }
|
||||
// catch(InterruptedException ie)
|
||||
// {
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// catch(IOException ioe)
|
||||
// {
|
||||
// mLog.error("Error", ioe);
|
||||
// }
|
||||
//
|
||||
// recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public static void main(String[] args)
|
||||
// {
|
||||
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_3_TS0_65084_6591007.mbe";
|
||||
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe";
|
||||
//
|
||||
@@ -118,5 +106,5 @@ public class MBECallSequenceConverter
|
||||
// {
|
||||
// mLog.error("Error", ioe);
|
||||
// }
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IAudioConverter
|
||||
@@ -27,7 +25,7 @@ public interface IAudioConverter
|
||||
/**
|
||||
* Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
|
||||
*/
|
||||
public byte[] convert(List<ReusableAudioPacket> audioPackets);
|
||||
public byte[] convert(List<float[]> audioBuffers);
|
||||
|
||||
/**
|
||||
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the
|
||||
|
||||
@@ -20,7 +20,6 @@ package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.audio.AudioUtils;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import net.sourceforge.lame.lowlevel.LameEncoder;
|
||||
import net.sourceforge.lame.mp3.Lame;
|
||||
import net.sourceforge.lame.mp3.MPEGMode;
|
||||
@@ -54,7 +53,7 @@ public class MP3AudioConverter implements IAudioConverter
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] convert(List<ReusableAudioPacket> audioPackets)
|
||||
public byte[] convert(List<float[]> audioPackets)
|
||||
{
|
||||
mMP3Stream.reset();
|
||||
|
||||
@@ -84,6 +83,36 @@ public class MP3AudioConverter implements IAudioConverter
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] convertAudio(List<float[]> audioBuffers)
|
||||
{
|
||||
mMP3Stream.reset();
|
||||
|
||||
byte[] pcmBytes = AudioUtils.convert(audioBuffers);
|
||||
|
||||
int pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length);
|
||||
|
||||
int mp3BufferSize = 0;
|
||||
|
||||
int pcmBytesPosition = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer)))
|
||||
{
|
||||
pcmBytesPosition += pcmBufferSize;
|
||||
pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition);
|
||||
mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize);
|
||||
}
|
||||
|
||||
return mMP3Stream.toByteArray();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("There was an error converting audio to MP3: " + e.getMessage());
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] flush()
|
||||
{
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
******************************************************************************/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.record.mp3.MP3Recorder;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -31,10 +28,11 @@ import java.util.List;
|
||||
public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
|
||||
public static final int MP3_BIT_RATE = 16;
|
||||
public static final boolean CONSTANT_BIT_RATE = false;
|
||||
|
||||
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
|
||||
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3_BIT_RATE, CONSTANT_BIT_RATE);
|
||||
private byte[] mPreviousPartialFrameData;
|
||||
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("MP3 Silence Generator");
|
||||
|
||||
/**
|
||||
* Generates MP3 audio silence frames
|
||||
@@ -46,13 +44,10 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
public byte[] generate(long duration)
|
||||
{
|
||||
int length = (int)(duration * 8); //8000 Hz sample rate
|
||||
ReusableAudioPacket silencePacket = mAudioPacketQueue.getBuffer(length);
|
||||
Arrays.fill(silencePacket.getAudioSamples(), 0.0f);
|
||||
|
||||
List<ReusableAudioPacket> silencePackets = new ArrayList<>();
|
||||
silencePackets.add(silencePacket);
|
||||
|
||||
byte[] frameData = mGenerator.convert(silencePackets);
|
||||
List<float[]> silenceBuffers = new ArrayList<>();
|
||||
silenceBuffers.add(new float[length]);
|
||||
byte[] frameData = mGenerator.convert(silenceBuffers);
|
||||
|
||||
frameData = merge(mPreviousPartialFrameData, frameData);
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ import io.github.dsheirer.audio.convert.thumbdv.message.response.DecodeSpeechRes
|
||||
import io.github.dsheirer.audio.convert.thumbdv.message.response.ReadyResponse;
|
||||
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -82,14 +82,14 @@ public class ThumbDv implements AutoCloseable
|
||||
private ScheduledFuture mAudioDecodeProcessorHandle;
|
||||
private LinkedTransferQueue<DecodeSpeechRequest> mDecodeSpeechRequests = new LinkedTransferQueue<>();
|
||||
private AudioProtocol mAudioProtocol;
|
||||
private Listener<ReusableAudioPacket> mAudioPacketListener;
|
||||
private ReusableAudioPacketQueue mReusableAudioPacketQueue = new ReusableAudioPacketQueue("ThumbDv");
|
||||
private Listener<ReusableFloatBuffer> mAudioBufferListener;
|
||||
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("ThumbDv");
|
||||
private boolean mStarted;
|
||||
|
||||
public ThumbDv(AudioProtocol audioProtocol, Listener<ReusableAudioPacket> listener)
|
||||
public ThumbDv(AudioProtocol audioProtocol, Listener<ReusableFloatBuffer> listener)
|
||||
{
|
||||
mAudioProtocol = audioProtocol;
|
||||
mAudioPacketListener = listener;
|
||||
mAudioBufferListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,12 +206,11 @@ public class ThumbDv implements AutoCloseable
|
||||
{
|
||||
AmbeMessage message = AmbeMessageFactory.getMessage(bytes);
|
||||
|
||||
if(message instanceof DecodeSpeechResponse && mAudioPacketListener != null)
|
||||
if(message instanceof DecodeSpeechResponse && mAudioBufferListener != null)
|
||||
{
|
||||
float[] samples = ((DecodeSpeechResponse)message).getSamples();
|
||||
ReusableAudioPacket audioPacket = mReusableAudioPacketQueue.getBuffer(samples.length);
|
||||
audioPacket.loadAudioFrom(samples);
|
||||
mAudioPacketListener.receive(audioPacket);
|
||||
ReusableFloatBuffer audioBuffer = mReusableBufferQueue.getBuffer(samples, System.currentTimeMillis());
|
||||
mAudioBufferListener.receive(audioBuffer);
|
||||
}
|
||||
else if(message instanceof ReadyResponse && mAudioDecodeProcessorHandle == null)
|
||||
{
|
||||
@@ -457,7 +456,7 @@ public class ThumbDv implements AutoCloseable
|
||||
|
||||
mLog.debug("Starting thumb dv thread(s)");
|
||||
|
||||
final Listener<ReusableAudioPacket> listener = reusableAudioPacket -> {
|
||||
final Listener<ReusableFloatBuffer> listener = reusableAudioPacket -> {
|
||||
mLog.info("Got an audio packet!");
|
||||
reusableAudioPacket.decrementUserCount();
|
||||
};
|
||||
|
||||
@@ -52,6 +52,8 @@ import java.awt.EventQueue;
|
||||
import java.awt.Font;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
|
||||
{
|
||||
@@ -84,6 +86,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
private Identifier mIdentifier;
|
||||
private List<Alias> mAliases = Collections.EMPTY_LIST;
|
||||
private AliasModel mAliasModel;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
|
||||
public AudioChannelPanel(IconManager iconManager, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||
AudioOutput audioOutput, AliasModel aliasModel)
|
||||
@@ -127,6 +130,12 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
{
|
||||
//Deregister from receiving preference update notifications
|
||||
MyEventBus.getEventBus().unregister(this);
|
||||
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.removeAudioEventListener(this);
|
||||
mAudioOutput.removeAudioMetadataListener();
|
||||
}
|
||||
}
|
||||
|
||||
private void init()
|
||||
@@ -193,13 +202,24 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
*/
|
||||
private void resetLabels()
|
||||
{
|
||||
boolean updated = mIdentifier != null;
|
||||
mIdentifier = null;
|
||||
mAliases = Collections.EMPTY_LIST;
|
||||
//Protect access to mIdentifier and mAliases
|
||||
mLock.lock();
|
||||
|
||||
if(updated)
|
||||
try
|
||||
{
|
||||
updateLabels();
|
||||
boolean updated = mIdentifier != null;
|
||||
mIdentifier = null;
|
||||
mAliases = Collections.EMPTY_LIST;
|
||||
|
||||
//Hold the lock through the label update
|
||||
if(updated)
|
||||
{
|
||||
updateLabels();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,11 +241,28 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
|
||||
boolean updated = false;
|
||||
|
||||
if(toIds.size() == 1)
|
||||
{
|
||||
Identifier currentIdentifier = mIdentifier;
|
||||
//Protect access to mIdentifier and mAliases
|
||||
mLock.lock();
|
||||
|
||||
if(currentIdentifier == null || currentIdentifier != toIds.get(0))
|
||||
try
|
||||
{
|
||||
if(toIds.size() == 1)
|
||||
{
|
||||
Identifier currentIdentifier = mIdentifier;
|
||||
|
||||
if(currentIdentifier == null || currentIdentifier != toIds.get(0))
|
||||
{
|
||||
mIdentifier = toIds.get(0);
|
||||
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
|
||||
|
||||
if(aliasList != null)
|
||||
{
|
||||
mAliases = aliasList.getAliases(mIdentifier);
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mIdentifier = toIds.get(0);
|
||||
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
|
||||
@@ -236,69 +273,64 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mIdentifier = toIds.get(0);
|
||||
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
|
||||
|
||||
if(aliasList != null)
|
||||
//Hold the lock through the label update
|
||||
if(updated)
|
||||
{
|
||||
mAliases = aliasList.getAliases(mIdentifier);
|
||||
updateLabels();
|
||||
}
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if(updated)
|
||||
finally
|
||||
{
|
||||
updateLabels();
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the alias label with text and icon from the alias. Note: this
|
||||
* does not occur on the Swing event thread -- wrap any calls to this
|
||||
* method with an event thread call.
|
||||
* Updates the alias label with text and icon from the alias.
|
||||
*/
|
||||
private void updateLabels()
|
||||
{
|
||||
String identifier = null;
|
||||
String iconName = null;
|
||||
|
||||
if(mAliases.size() == 1)
|
||||
{
|
||||
identifier = mAliases.get(0).getName();
|
||||
iconName = mAliases.get(0).getIconName();
|
||||
}
|
||||
else if(mAliases.size() > 1)
|
||||
{
|
||||
identifier = Joiner.on(", ").skipNulls().join(mAliases);
|
||||
}
|
||||
//Protect access to mIdentifier and mAliases
|
||||
mLock.lock();
|
||||
|
||||
if(identifier == null && mIdentifier != null)
|
||||
try
|
||||
{
|
||||
identifier = mTalkgroupFormatPreference.format(mIdentifier);
|
||||
}
|
||||
|
||||
if(identifier == null)
|
||||
{
|
||||
identifier = "-----";
|
||||
}
|
||||
|
||||
final ImageIcon icon = iconName != null ? mIconManager.getIcon(iconName, 18) : null;
|
||||
|
||||
final String identifierText = identifier;
|
||||
|
||||
EventQueue.invokeLater(new Runnable()
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
if(mAliases.size() == 1)
|
||||
{
|
||||
identifier = mAliases.get(0).getName();
|
||||
iconName = mAliases.get(0).getIconName();
|
||||
}
|
||||
else if(mAliases.size() > 1)
|
||||
{
|
||||
identifier = Joiner.on(", ").skipNulls().join(mAliases);
|
||||
}
|
||||
|
||||
if(identifier == null && mIdentifier != null)
|
||||
{
|
||||
identifier = mTalkgroupFormatPreference.format(mIdentifier);
|
||||
}
|
||||
|
||||
if(identifier == null)
|
||||
{
|
||||
identifier = "-----";
|
||||
}
|
||||
|
||||
final ImageIcon icon = iconName != null ? mIconManager.getIcon(iconName, 18) : null;
|
||||
final String identifierText = identifier;
|
||||
|
||||
EventQueue.invokeLater(() -> {
|
||||
mIdentifierLabel.setText(identifierText);
|
||||
mIconLabel.setIcon(icon);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,14 +355,16 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
||||
switch(colorSetting.getColorSettingName())
|
||||
{
|
||||
case CHANNEL_STATE_LABEL_DECODER:
|
||||
if(mIdentifierLabel != null)
|
||||
{
|
||||
mIdentifierLabel.setForeground(mLabelColor);
|
||||
}
|
||||
if(mIconLabel != null)
|
||||
{
|
||||
mIconLabel.setForeground(mLabelColor);
|
||||
}
|
||||
EventQueue.invokeLater(() -> {
|
||||
if(mIdentifierLabel != null)
|
||||
{
|
||||
mIdentifierLabel.setForeground(mLabelColor);
|
||||
}
|
||||
if(mIconLabel != null)
|
||||
{
|
||||
mIconLabel.setForeground(mLabelColor);
|
||||
}
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -21,13 +21,23 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.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 io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -42,42 +52,41 @@ import javax.sound.sampled.LineUnavailableException;
|
||||
import javax.sound.sampled.Mixer;
|
||||
import javax.sound.sampled.SourceDataLine;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public abstract class AudioOutput implements Listener<ReusableAudioPacket>, LineListener
|
||||
/**
|
||||
* 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>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
|
||||
private LinkedTransferQueue<ReusableAudioPacket> mBuffer = new LinkedTransferQueue<>();
|
||||
private int mBufferStartThreshold;
|
||||
private int mBufferStopThreshold;
|
||||
private static IdentifierCollection EMPTY_IDENTIFIER_COLLECTION = new IdentifierCollection(0);
|
||||
static
|
||||
{
|
||||
EMPTY_IDENTIFIER_COLLECTION.setUpdated(true);
|
||||
}
|
||||
|
||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||
|
||||
private ScheduledFuture<?> mProcessorTask;
|
||||
|
||||
private ScheduledFuture<?> mProcessorFuture;
|
||||
private SourceDataLine mOutput;
|
||||
private Mixer mMixer;
|
||||
private MixerChannel mMixerChannel;
|
||||
private FloatControl mGainControl;
|
||||
private BooleanControl mMuteControl;
|
||||
|
||||
private AudioEvent mAudioStartEvent;
|
||||
private AudioEvent mAudioStopEvent;
|
||||
|
||||
private boolean mCanProcessAudio = false;
|
||||
private long mLastActivity = System.currentTimeMillis();
|
||||
private AudioSegment mCurrentAudioSegment;
|
||||
private AudioSegment mNextAudioSegment;
|
||||
private ReentrantLock mLock = new ReentrantLock();
|
||||
private int mCurrentBufferIndex = 0;
|
||||
private UserPreferences mUserPreferences;
|
||||
private BooleanProperty mEmptyProperty = new SimpleBooleanProperty(true);
|
||||
private IntegerProperty mAudioPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
||||
private ByteBuffer mAudioSegmentStartTone;
|
||||
private ByteBuffer mAudioSegmentPreemptTone;
|
||||
private ByteBuffer mAudioSegmentDropTone;
|
||||
private boolean mRunning = false;
|
||||
|
||||
/**
|
||||
* Single audio channel playback with automatic starting and stopping of the
|
||||
@@ -94,10 +103,11 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
* @param requestedBufferSize of approximately 1 second of audio
|
||||
*/
|
||||
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
|
||||
int requestedBufferSize)
|
||||
int requestedBufferSize, UserPreferences userPreferences)
|
||||
{
|
||||
mMixer = mixer;
|
||||
mMixerChannel = mixerChannel;
|
||||
mUserPreferences = userPreferences;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -139,46 +149,395 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
}
|
||||
|
||||
/* Run the queue processor task every 40 milliseconds or 25 times a second */
|
||||
mProcessorTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(),
|
||||
0, 40, TimeUnit.MILLISECONDS);
|
||||
//Run the queue processor task every 100 milliseconds or 10 times a second
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED,
|
||||
getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED,
|
||||
getChannelName());
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
mLog.error("Couldn't obtain audio source data line for "
|
||||
+ "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]");
|
||||
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
}
|
||||
|
||||
updateToneInsertionAudioClips();
|
||||
|
||||
//Register to receive directory preference update notifications so we can update the preference items
|
||||
MyEventBus.getEventBus().register(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean property that indicates if this audio output has an audio segment queued or in process of playback.
|
||||
*/
|
||||
public BooleanProperty emptyProperty()
|
||||
{
|
||||
return mEmptyProperty;
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio playback priority of the current audio segment or default audio priority if nothing is currently loaded.
|
||||
*/
|
||||
public IntegerProperty audioPriorityProperty()
|
||||
{
|
||||
return mAudioPriority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the audio segment for playback.
|
||||
*
|
||||
* Note: if a segment is currently playing and another audio segment is already queued for playback, invoking
|
||||
* this method will overwrite the queued segment with the argument.
|
||||
*
|
||||
* @param audioSegment to schedule for playback.
|
||||
*/
|
||||
public void play(AudioSegment audioSegment)
|
||||
{
|
||||
if(audioSegment != null)
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
if(mNextAudioSegment != null)
|
||||
{
|
||||
mNextAudioSegment.decrementConsumerCount();
|
||||
mNextAudioSegment = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mNextAudioSegment = audioSegment;
|
||||
mEmptyProperty.set(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void reset()
|
||||
/**
|
||||
* Indicates if the audio segment is linked to the current playback audio segment
|
||||
*/
|
||||
public boolean isLinkedTo(AudioSegment audioSegment)
|
||||
{
|
||||
broadcast(new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName()));
|
||||
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)
|
||||
{
|
||||
broadcast(identifierCollection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates audio segment start, drop and preempt tone insertion clips
|
||||
*/
|
||||
private void updateToneInsertionAudioClips()
|
||||
{
|
||||
mAudioSegmentStartTone = null;
|
||||
mAudioSegmentPreemptTone = null;
|
||||
mAudioSegmentDropTone = null;
|
||||
|
||||
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
|
||||
|
||||
if(start != null)
|
||||
{
|
||||
mAudioSegmentStartTone = convert(start);
|
||||
}
|
||||
|
||||
float[] preempt = mUserPreferences.getPlaybackPreference().getPreemptTone();
|
||||
|
||||
if(preempt != null)
|
||||
{
|
||||
mAudioSegmentPreemptTone = convert(preempt);
|
||||
}
|
||||
|
||||
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
|
||||
|
||||
if(drop != null)
|
||||
{
|
||||
mAudioSegmentDropTone = convert(drop);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of a processed audio segment and notifies the originator that processing of the segment is complete
|
||||
* by decrementing the consumer count.
|
||||
* @param audioSegment to dispose
|
||||
*/
|
||||
private void dispose(AudioSegment audioSegment)
|
||||
{
|
||||
mAudioPriority.unbind();
|
||||
|
||||
if(audioSegment != null)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
audioSegment.removeIdentifierUpdateNotificationListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tone indicating that a new audio segment is starting
|
||||
*/
|
||||
private ByteBuffer getAudioSegmentStartTone()
|
||||
{
|
||||
return mAudioSegmentStartTone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tone indicating that the current audio segment playback has been preempted for a higher priority
|
||||
* audio segment that is now starting.
|
||||
*/
|
||||
private ByteBuffer getAudioSegmentPreemptionTone()
|
||||
{
|
||||
return mAudioSegmentPreemptTone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a tone indicating that the current audio segment playback has been dropped because the audio segment
|
||||
* has been flagged as Do Not Monitor after playback has started.
|
||||
*/
|
||||
private ByteBuffer getAudioSegmentDropTone()
|
||||
{
|
||||
return mAudioSegmentDropTone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the audio buffer data to the source data line. If the data line is not currently playing, write as
|
||||
* much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual
|
||||
* buffer content to the data line as a blocking call.
|
||||
*
|
||||
* @param buffer of audio to playback
|
||||
*/
|
||||
private void playAudio(ByteBuffer buffer)
|
||||
{
|
||||
if(buffer != null)
|
||||
{
|
||||
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())
|
||||
{
|
||||
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);
|
||||
|
||||
checkStart();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
|
||||
* by a threaded processor repeatedly to playback the current audio segment and check for and start a newly
|
||||
* assigned audio segment. It also handles starting and stopping the playback source data line to avoid audio
|
||||
* discontinuities due to buffer underruns.
|
||||
*/
|
||||
private void processAudio()
|
||||
{
|
||||
if(mNextAudioSegment != null)
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
//Remove an assigned audio segment that's subsequently flagged as do not monitor before playback
|
||||
if(mNextAudioSegment.isDoNotMonitor())
|
||||
{
|
||||
mNextAudioSegment = null;
|
||||
|
||||
if(mCurrentAudioSegment == null)
|
||||
{
|
||||
mEmptyProperty.set(true);
|
||||
}
|
||||
}
|
||||
//For linked audio segments, allow the linked segment to complete first before assigning the next
|
||||
else if(mNextAudioSegment.isLinked())
|
||||
{
|
||||
if(mCurrentAudioSegment == null)
|
||||
{
|
||||
mCurrentAudioSegment = mNextAudioSegment;
|
||||
mNextAudioSegment = null;
|
||||
mCurrentBufferIndex = 0;
|
||||
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
|
||||
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||
}
|
||||
else
|
||||
{
|
||||
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Insert audio segment start or audio priority preemption bonk tone
|
||||
if(mCurrentAudioSegment == null)
|
||||
{
|
||||
playAudio(getAudioSegmentStartTone());
|
||||
}
|
||||
else if(mCurrentBufferIndex > 0 &&
|
||||
(!mCurrentAudioSegment.completeProperty().get() ||
|
||||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount()))
|
||||
{
|
||||
playAudio(getAudioSegmentPreemptionTone());
|
||||
}
|
||||
else
|
||||
{
|
||||
playAudio(getAudioSegmentStartTone());
|
||||
}
|
||||
|
||||
//Close current audio segment
|
||||
dispose(mCurrentAudioSegment);
|
||||
mCurrentAudioSegment = mNextAudioSegment;
|
||||
mNextAudioSegment = null;
|
||||
mCurrentBufferIndex = 0;
|
||||
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
|
||||
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
|
||||
broadcast(mCurrentAudioSegment.getIdentifierCollection());
|
||||
}
|
||||
else
|
||||
{
|
||||
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if(mCurrentAudioSegment != null)
|
||||
{
|
||||
//Check for completed audio segment or a segment flagged as Do Not Monitor
|
||||
if(mCurrentAudioSegment.isDoNotMonitor() || (mCurrentAudioSegment.completeProperty().get() &&
|
||||
mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
|
||||
{
|
||||
if(mCurrentAudioSegment.isDoNotMonitor())
|
||||
{
|
||||
playAudio(getAudioSegmentDropTone());
|
||||
}
|
||||
|
||||
dispose(mCurrentAudioSegment);
|
||||
mCurrentAudioSegment = null;
|
||||
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(mNextAudioSegment == null)
|
||||
{
|
||||
mEmptyProperty.set(true);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//Process any new buffers that have been added to the audio segment. If a next audio segment gets assigned
|
||||
//while processing, exit the loop so that we can evaluate the next for higher priority preempt. If the next
|
||||
//segment is a linked segment, ignore it so that we can close out the current segment.
|
||||
while((mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
|
||||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
|
||||
!mCurrentAudioSegment.isDoNotMonitor())
|
||||
{
|
||||
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
|
||||
|
||||
if(audioBuffer != null)
|
||||
{
|
||||
ByteBuffer audio = convert(audioBuffer);
|
||||
playAudio(audio);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkStop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares this audio output for disposal.
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
mCanProcessAudio = false;
|
||||
|
||||
if(mProcessorTask != null)
|
||||
if(mProcessorFuture != null)
|
||||
{
|
||||
mProcessorTask.cancel(true);
|
||||
mProcessorFuture.cancel(true);
|
||||
}
|
||||
|
||||
mProcessorTask = null;
|
||||
mProcessorFuture = null;
|
||||
|
||||
mBuffer.clear();
|
||||
mLock.lock();
|
||||
|
||||
mAudioEventBroadcaster.dispose();
|
||||
mAudioEventBroadcaster = null;
|
||||
try
|
||||
{
|
||||
if(mNextAudioSegment != null)
|
||||
{
|
||||
mNextAudioSegment.decrementConsumerCount();
|
||||
mNextAudioSegment = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
|
||||
dispose(mCurrentAudioSegment);
|
||||
mCurrentAudioSegment = null;
|
||||
|
||||
mAudioEventBroadcaster.clear();
|
||||
mIdentifierCollectionListener = null;
|
||||
|
||||
if(mOutput != null)
|
||||
@@ -192,10 +551,9 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into a byte buffer format appropriate for
|
||||
* the underlying source data line.
|
||||
* Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line.
|
||||
*/
|
||||
protected abstract ByteBuffer convert(ReusableAudioPacket packet);
|
||||
protected abstract ByteBuffer convert(float[] buffer);
|
||||
|
||||
/**
|
||||
* Audio output channel name
|
||||
@@ -229,7 +587,7 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
/**
|
||||
* Broadcasts an audio event to the registered listener
|
||||
*/
|
||||
private void broadcast(AudioEvent audioEvent)
|
||||
private void broadcastAudioEvent(AudioEvent audioEvent)
|
||||
{
|
||||
mAudioEventBroadcaster.broadcast(audioEvent);
|
||||
}
|
||||
@@ -243,6 +601,9 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
mIdentifierCollectionListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the current audio metadata listener
|
||||
*/
|
||||
public void removeAudioMetadataListener()
|
||||
{
|
||||
mIdentifierCollectionListener = null;
|
||||
@@ -260,138 +621,46 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp of either last buffer received or last buffer processed
|
||||
* 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
|
||||
*/
|
||||
public long getLastActivityTimestamp()
|
||||
private void checkStart()
|
||||
{
|
||||
return mLastActivity;
|
||||
if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold)
|
||||
{
|
||||
mOutput.start();
|
||||
mRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last activity timestamp to current system time
|
||||
* 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
|
||||
*/
|
||||
public void updateTimestamp()
|
||||
private void checkStop()
|
||||
{
|
||||
mLastActivity = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(ReusableAudioPacket packet)
|
||||
{
|
||||
if(mCanProcessAudio)
|
||||
if(mRunning)
|
||||
{
|
||||
//Update the activity timestamp so that this audio output doesn't
|
||||
//get disconnected before it starts processing the audio stream
|
||||
updateTimestamp();
|
||||
|
||||
mBuffer.add(packet);
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
public class BufferProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
private List<ReusableAudioPacket> mAudioPackets = new ArrayList<ReusableAudioPacket>();
|
||||
|
||||
|
||||
public BufferProcessor()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
/* The processing flag ensures that only one instance of the
|
||||
* processor can run at any given time */
|
||||
if(mProcessing.compareAndSet(false, true))
|
||||
{
|
||||
mBuffer.drainTo(mAudioPackets);
|
||||
|
||||
for(ReusableAudioPacket packet : mAudioPackets)
|
||||
{
|
||||
if(packet.getType() == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
broadcast(packet.getIdentifierCollection());
|
||||
|
||||
ByteBuffer buffer = convert(packet);
|
||||
|
||||
int wrote = 0;
|
||||
|
||||
if(!mOutput.isRunning())
|
||||
{
|
||||
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);
|
||||
|
||||
checkStart();
|
||||
}
|
||||
|
||||
if(mOutput.isRunning() && wrote < buffer.array().length)
|
||||
{
|
||||
//Blocking write
|
||||
wrote += mOutput.write(buffer.array(), wrote,
|
||||
buffer.array().length - wrote);
|
||||
}
|
||||
|
||||
updateTimestamp();
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
mAudioPackets.clear();
|
||||
|
||||
checkStop();
|
||||
|
||||
mProcessing.set(false);
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error while processing audio buffers", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts audio playback once audio buffer is almost full and remaining
|
||||
* capacity falls below the start threshold.
|
||||
*/
|
||||
private void checkStart()
|
||||
{
|
||||
if(mCanProcessAudio &&
|
||||
!mOutput.isRunning() &&
|
||||
mOutput.available() <= mBufferStartThreshold)
|
||||
{
|
||||
mOutput.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private void checkStop()
|
||||
{
|
||||
if(mCanProcessAudio && mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
|
||||
//If the output buffer falls below the threshold then drain the output and stop playback
|
||||
if(mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
|
||||
{
|
||||
mOutput.drain();
|
||||
mOutput.stop();
|
||||
broadcast(EMPTY_IDENTIFIER_COLLECTION);
|
||||
mRunning = false;
|
||||
}
|
||||
//If output playback stopped on its own because the buffer emptied, then cleanup
|
||||
else if(!mOutput.isRunning())
|
||||
{
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
//If we stopped audio playback, broadcast a null identifier to clear the gui panel
|
||||
if(!mRunning)
|
||||
{
|
||||
broadcast(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,9 +673,7 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
if(mMuteControl != null)
|
||||
{
|
||||
mMuteControl.setValue(muted);
|
||||
|
||||
broadcast(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED :
|
||||
AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
||||
broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,14 +698,17 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
return mGainControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio output has a gain control available
|
||||
*/
|
||||
public boolean hasGainControl()
|
||||
{
|
||||
return mGainControl != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitors the source data line playback state and broadcasts audio events
|
||||
* to the registered listener as the state changes
|
||||
* Monitors the source data line playback state and broadcasts audio events to the registered listener as the
|
||||
* state changes
|
||||
*/
|
||||
@Override
|
||||
public void update(LineEvent event)
|
||||
@@ -454,4 +724,23 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
||||
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Runnable audio segment processor
|
||||
*/
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
processAudio();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error while processing audio buffers", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,24 @@ 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.PreferenceEditorViewRequest;
|
||||
import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.SourceManager;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
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;
|
||||
@@ -51,6 +58,7 @@ import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.util.List;
|
||||
|
||||
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
{
|
||||
@@ -169,34 +177,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||
JPopupMenu popup = new JPopupMenu();
|
||||
|
||||
/* Audio mixer/output selection menus */
|
||||
JMenu outputMenu = new JMenu("Audio Output");
|
||||
|
||||
MixerChannelConfiguration[] mixerConfigurations =
|
||||
mSourceManager.getMixerManager().getOutputMixers();
|
||||
|
||||
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
|
||||
JMenuItem outputMenu = new JMenuItem("Configure ...");
|
||||
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
|
||||
outputMenu.setIcon(icon);
|
||||
outputMenu.addActionListener(new ActionListener()
|
||||
{
|
||||
MixerSelectionItem mixerItem = new MixerSelectionItem(mixerConfig);
|
||||
|
||||
try
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e)
|
||||
{
|
||||
MixerChannelConfiguration current = mController.getMixerChannelConfiguration();
|
||||
|
||||
if(current != null && current.equals(mixerConfig))
|
||||
{
|
||||
mixerItem.setSelected(true);
|
||||
}
|
||||
MyEventBus.getEventBus().post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK));
|
||||
}
|
||||
catch(AudioException e)
|
||||
{
|
||||
mLog.error("Error while detecting current mixer "
|
||||
+ "channel configuration", e);
|
||||
}
|
||||
|
||||
outputMenu.add(mixerItem);
|
||||
}
|
||||
|
||||
});
|
||||
popup.add(outputMenu);
|
||||
popup.add(new JPopupMenu.Separator());
|
||||
|
||||
/* Audio output mute and volume control */
|
||||
for(AudioOutput output : mController.getAudioOutputs())
|
||||
|
||||
@@ -19,13 +19,17 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioEvent;
|
||||
import io.github.dsheirer.audio.AudioException;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.audio.IAudioController;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
@@ -38,113 +42,206 @@ import javax.sound.sampled.Mixer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAudioController
|
||||
/**
|
||||
* Manages scheduling and playback of audio segments to the local users audio system.
|
||||
*/
|
||||
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
|
||||
|
||||
public static final int AUDIO_TIMEOUT = 1000; //1 second
|
||||
|
||||
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
|
||||
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
||||
|
||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
||||
|
||||
private LinkedTransferQueue<ReusableAudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
|
||||
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
|
||||
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
|
||||
private AudioOutputConnection mLowestPriorityConnection;
|
||||
private int mAvailableConnectionCount;
|
||||
|
||||
private Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
|
||||
|
||||
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||
|
||||
private ScheduledFuture<?> mProcessingTask;
|
||||
private MixerManager mMixerManager;
|
||||
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<>();
|
||||
|
||||
/**
|
||||
* Processes all audio produced by the decoding channels and routes audio
|
||||
* packets to any combination of outputs based on any alias audio routing
|
||||
* options specified by the user.
|
||||
* Constructs an instance.
|
||||
*
|
||||
* @param userPreferences for audio playback preferences
|
||||
*/
|
||||
public AudioPlaybackManager(MixerManager mixerManager)
|
||||
public AudioPlaybackManager(UserPreferences userPreferences)
|
||||
{
|
||||
mMixerManager = mixerManager;
|
||||
mUserPreferences = userPreferences;
|
||||
MyEventBus.getEventBus().register(this);
|
||||
|
||||
loadSettings();
|
||||
}
|
||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||
|
||||
/**
|
||||
* Loads the saved mixer configuration or a default configuration for audio playback.
|
||||
*/
|
||||
private void loadSettings()
|
||||
{
|
||||
MixerChannelConfiguration configuration = null;
|
||||
|
||||
SystemProperties properties = SystemProperties.getInstance();
|
||||
|
||||
Mixer defaultMixer = AudioSystem.getMixer(null);
|
||||
|
||||
String mixer = properties.get(AUDIO_MIXER_PROPERTY, defaultMixer.getMixerInfo().getName());
|
||||
|
||||
String channels = properties.get(AUDIO_CHANNELS_PROPERTY, MixerChannel.MONO.name());
|
||||
|
||||
MixerChannelConfiguration[] mixerConfigurations = mMixerManager.getOutputMixers();
|
||||
|
||||
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
|
||||
if(configuration != null)
|
||||
{
|
||||
if(mixerConfig.matches(mixer, channels))
|
||||
{
|
||||
configuration = mixerConfig;
|
||||
}
|
||||
}
|
||||
|
||||
if(configuration == null)
|
||||
{
|
||||
configuration = getDefaultConfiguration();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(configuration, false);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Couldn't set stored audio mixer/channel configuration - using default", e);
|
||||
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(getDefaultConfiguration());
|
||||
setMixerChannelConfiguration(configuration);
|
||||
}
|
||||
catch(Exception e2)
|
||||
catch(AudioException ae)
|
||||
{
|
||||
mLog.error("Couldn't set default audio mixer/channel configuration - no audio will be available", e2);
|
||||
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);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.warn("No audio output devices available");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a default audio playback configuration with a mono audio playback channel.
|
||||
* Receives audio segments from channel audio modules.
|
||||
* @param audioSegment
|
||||
*/
|
||||
private MixerChannelConfiguration getDefaultConfiguration()
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
/* Use the system default mixer and mono channel as default startup */
|
||||
Mixer defaultMixer = AudioSystem.getMixer(null);
|
||||
mNewAudioSegmentQueue.add(audioSegment);
|
||||
}
|
||||
|
||||
return new MixerChannelConfiguration(defaultMixer, MixerChannel.MONO);
|
||||
/**
|
||||
* Processes new audio segments and automatically assigns them to audio outputs.
|
||||
*
|
||||
* Note: this method is intended to be repeatedly invoked by a scheduled processing thread.
|
||||
*/
|
||||
private void processAudioSegments()
|
||||
{
|
||||
//Process new audio segments queue. If segment has audio, queue it for replay, otherwise place in pending queue
|
||||
AudioSegment newSegment = mNewAudioSegmentQueue.poll();
|
||||
|
||||
while(newSegment != null)
|
||||
{
|
||||
if(newSegment.hasAudio())
|
||||
{
|
||||
mAudioSegments.add(newSegment);
|
||||
}
|
||||
else
|
||||
{
|
||||
mPendingAudioSegments.add(newSegment);
|
||||
}
|
||||
|
||||
newSegment = mNewAudioSegmentQueue.poll();
|
||||
}
|
||||
|
||||
//Transfer pending audio segments that now have audio or that completed without ever having audio
|
||||
if(!mPendingAudioSegments.isEmpty())
|
||||
{
|
||||
Iterator<AudioSegment> it = mPendingAudioSegments.iterator();
|
||||
|
||||
AudioSegment audioSegment;
|
||||
|
||||
while(it.hasNext())
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.hasAudio())
|
||||
{
|
||||
//Queue it up for replay
|
||||
it.remove();
|
||||
mAudioSegments.add(audioSegment);
|
||||
}
|
||||
else if(audioSegment.completeProperty().get())
|
||||
{
|
||||
//Rare situation: the audio segment completed but never had audio ... dispose it
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Process all audio segments that have audio
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
Iterator<AudioSegment> it = mAudioSegments.iterator();
|
||||
AudioSegment audioSegment;
|
||||
|
||||
//Remove any audio segments flagged as do not monitor. Don't remove completed segments yet, because
|
||||
//we want to give them a brief chance at playback. Automatically assign linked audio segments to the
|
||||
//current audio output for audio continuity
|
||||
while(it.hasNext())
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDoNotMonitor())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
else if(audioSegment.isLinked())
|
||||
{
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
{
|
||||
it.remove();
|
||||
audioOutput.play(audioSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Sort audio segments by playback priority and assign to empty audio outputs
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
Collections.sort(mAudioSegments, Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
|
||||
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
if(audioOutput.emptyProperty().get())
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Preempt ongoing audio segment playback when higher priority audio segments are available
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
if(mAudioSegments.get(0).monitorPriorityProperty().get() < audioOutput.audioPriorityProperty().get())
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Remove any audio segments marked as complete that didn't get assigned to an output
|
||||
it = mAudioSegments.iterator(); //reset the iterator
|
||||
while(it.hasNext())
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.completeProperty().get())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
@@ -152,68 +249,34 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
if(mProcessingTask != null)
|
||||
{
|
||||
mProcessingTask.cancel(true);
|
||||
mProcessingTask = null;
|
||||
}
|
||||
|
||||
mAudioPacketQueue.clear();
|
||||
|
||||
mProcessingTask = null;
|
||||
|
||||
mChannelConnectionMap.clear();
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
connection.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputConnections.clear();
|
||||
mNewAudioSegmentQueue.clear();
|
||||
mAudioSegments.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
|
||||
* Receive user preference update notifications so that we can detect when the user changes the audio output
|
||||
* device in the user preferences editor.
|
||||
*/
|
||||
@Override
|
||||
public synchronized void receive(ReusableAudioPacket packet)
|
||||
@Subscribe
|
||||
public void preferenceUpdated(PreferenceType preferenceType)
|
||||
{
|
||||
mAudioPacketQueue.add(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks each audio channel assignment and disconnects any inactive connections
|
||||
*/
|
||||
private void disconnectInactiveChannelAssignments()
|
||||
{
|
||||
boolean changed = false;
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
if(preferenceType == PreferenceType.PLAYBACK)
|
||||
{
|
||||
if(connection.isInactive() && mChannelConnectionMap.containsKey(connection.getAudioChannelId()))
|
||||
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||
|
||||
if(configuration != null && !configuration.equals(mMixerChannelConfiguration))
|
||||
{
|
||||
mChannelConnectionMap.remove(connection.getAudioChannelId());
|
||||
connection.disconnect();
|
||||
mAvailableConnectionCount++;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(changed)
|
||||
{
|
||||
updateLowestPriorityAssignment();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the lowest priority channel connection where the a higher value indicates a lower priority.
|
||||
*/
|
||||
private void updateLowestPriorityAssignment()
|
||||
{
|
||||
mLowestPriorityConnection = null;
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
if(connection.isConnected() &&
|
||||
(mLowestPriorityConnection == null || mLowestPriorityConnection.getPriority() < connection.getPriority()))
|
||||
{
|
||||
mLowestPriorityConnection = connection;
|
||||
try
|
||||
{
|
||||
setMixerChannelConfiguration(configuration);
|
||||
}
|
||||
catch(AudioException ae)
|
||||
{
|
||||
mLog.error("Error changing audio output to [" + configuration.toString() + "]", ae);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -227,19 +290,7 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
@Override
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
|
||||
{
|
||||
setMixerChannelConfiguration(entry, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures audio playback to use the configuration specified in the entry argument.
|
||||
*
|
||||
* @param entry to use in configuring the audio playback setup.
|
||||
* @param saveSettings to save the audio playback configuration settings in the properties file.
|
||||
* @throws AudioException if there is an error
|
||||
*/
|
||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolean saveSettings) throws AudioException
|
||||
{
|
||||
if(entry != null && (entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO))
|
||||
if(entry != null)
|
||||
{
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
||||
|
||||
@@ -248,68 +299,37 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
mProcessingTask.cancel(true);
|
||||
}
|
||||
|
||||
disposeCurrentConfiguration();
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputs.clear();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer());
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(mono));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(mono.getChannelName(), mono);
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||
mAudioOutputs.add(mono);
|
||||
break;
|
||||
case STEREO:
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT);
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(left));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(left.getChannelName(), left);
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
||||
mAudioOutputs.add(left);
|
||||
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT);
|
||||
mAudioOutputConnections.add(new AudioOutputConnection(right));
|
||||
mAvailableConnectionCount++;
|
||||
mAudioOutputMap.put(right.getChannelName(), right);
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
||||
mAudioOutputs.add(right);
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel "
|
||||
+ "configuration: " + entry.getMixerChannel());
|
||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||
}
|
||||
|
||||
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioPacketProcessor(),
|
||||
0, 15, TimeUnit.MILLISECONDS);
|
||||
|
||||
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
||||
|
||||
if(saveSettings)
|
||||
{
|
||||
SystemProperties properties = SystemProperties.getInstance();
|
||||
properties.set(AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName());
|
||||
properties.set(AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name());
|
||||
}
|
||||
mMixerChannelConfiguration = entry;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all channel assignments and terminates all audio outputs in preparation for complete shutdown or change
|
||||
* to another mixer/channel configuration
|
||||
*/
|
||||
private void disposeCurrentConfiguration()
|
||||
{
|
||||
mChannelConnectionMap.clear();
|
||||
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
connection.dispose();
|
||||
}
|
||||
|
||||
mAvailableConnectionCount = 0;
|
||||
|
||||
mAudioOutputConnections.clear();
|
||||
|
||||
mAudioOutputMap.clear();
|
||||
|
||||
mLowestPriorityConnection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current audio playback mixer channel configuration setting.
|
||||
*/
|
||||
@@ -320,12 +340,12 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
}
|
||||
|
||||
/**
|
||||
* List of audio outputs available for the current mixer channel configuration
|
||||
* List of sorted audio outputs available for the current mixer channel configuration
|
||||
*/
|
||||
@Override
|
||||
public List<AudioOutput> getAudioOutputs()
|
||||
{
|
||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputMap.values());
|
||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputs);
|
||||
|
||||
Collections.sort(outputs, new Comparator<AudioOutput>()
|
||||
{
|
||||
@@ -358,228 +378,21 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an audio output connection for the packet if one is available, or overrides an existing lower priority
|
||||
* connection. Returns null if no connection is available for the audio packet.
|
||||
*
|
||||
* @param audioPacket from a decoding channel source
|
||||
* @return an audio output connection or null
|
||||
* Scheduled runnable to process incoming audio segments
|
||||
*/
|
||||
private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket)
|
||||
public class AudioSegmentProcessor implements Runnable
|
||||
{
|
||||
int audioChannelId = audioPacket.getAudioChannelId();
|
||||
|
||||
//Use an existing connection
|
||||
if(mChannelConnectionMap.containsKey(audioChannelId))
|
||||
{
|
||||
return mChannelConnectionMap.get(audioChannelId);
|
||||
}
|
||||
|
||||
//Connect to an unused, available connection
|
||||
if(mAvailableConnectionCount > 0)
|
||||
{
|
||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
||||
{
|
||||
if(connection.isDisconnected())
|
||||
{
|
||||
connection.connect(audioChannelId, audioPacket.getMonitoringPriority());
|
||||
mChannelConnectionMap.put(audioChannelId, connection);
|
||||
mAvailableConnectionCount--;
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Preempt an existing lower priority connection and connect when this is a higher priority packet
|
||||
else
|
||||
{
|
||||
int priority = audioPacket.getMonitoringPriority();
|
||||
|
||||
AudioOutputConnection connection = mLowestPriorityConnection;
|
||||
|
||||
if(connection != null && priority < connection.getPriority())
|
||||
{
|
||||
mChannelConnectionMap.remove(connection.getAudioChannelId());
|
||||
|
||||
connection.connect(audioChannelId, priority);
|
||||
|
||||
mChannelConnectionMap.put(audioChannelId, connection);
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public class AudioPacketProcessor implements Runnable
|
||||
{
|
||||
private List<ReusableAudioPacket> mPackets = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
try
|
||||
{
|
||||
disconnectInactiveChannelAssignments();
|
||||
|
||||
if(mAudioPacketQueue != null)
|
||||
{
|
||||
mAudioPacketQueue.drainTo(mPackets);
|
||||
|
||||
for(ReusableAudioPacket packet : mPackets)
|
||||
{
|
||||
/* Don't process any packet's marked as do not monitor */
|
||||
if(!packet.isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO)
|
||||
{
|
||||
AudioOutputConnection connection = getConnection(packet);
|
||||
|
||||
if(connection != null)
|
||||
{
|
||||
connection.receive(packet);
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
packet.decrementUserCount();
|
||||
}
|
||||
}
|
||||
|
||||
mPackets.clear();
|
||||
}
|
||||
processAudioSegments();
|
||||
}
|
||||
catch(Exception e)
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Encountered error while processing audio packets", e);
|
||||
|
||||
while(mPackets.size() > 0)
|
||||
{
|
||||
ReusableAudioPacket audioPacket = mPackets.remove(0);
|
||||
audioPacket.decrementUserCount();
|
||||
}
|
||||
mLog.error("Encountered error while processing audio segments", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio output connection manages a connection between a source and an audio
|
||||
* output and maintains current state information about the audio activity
|
||||
* received form the source.
|
||||
*/
|
||||
public class AudioOutputConnection
|
||||
{
|
||||
private static final int DISCONNECTED = Integer.MIN_VALUE;
|
||||
|
||||
private AudioOutput mAudioOutput;
|
||||
private int mPriority = 0;
|
||||
private int mAudioChannelId = DISCONNECTED;
|
||||
|
||||
public AudioOutputConnection(AudioOutput audioOutput)
|
||||
{
|
||||
mAudioOutput = audioOutput;
|
||||
}
|
||||
|
||||
public void receive(ReusableAudioPacket packet)
|
||||
{
|
||||
if(packet.hasIdentifierCollection() && packet.getAudioChannelId() == mAudioChannelId)
|
||||
{
|
||||
int priority = packet.getMonitoringPriority();
|
||||
|
||||
if(mPriority != priority)
|
||||
{
|
||||
mPriority = priority;
|
||||
updateLowestPriorityAssignment();
|
||||
}
|
||||
|
||||
if(mAudioOutput != null)
|
||||
{
|
||||
mAudioOutput.receive(packet);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(packet.hasIdentifierCollection())
|
||||
{
|
||||
mLog.error("Received audio packet from channel metadata [" + packet.getAudioChannelId() +
|
||||
"] however this assignment is currently connected to metadata [" + mAudioChannelId + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Received audio packet with no metadata - cannot route audio packet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates the audio output and prepares this connection for disposal
|
||||
*/
|
||||
public void dispose()
|
||||
{
|
||||
mAudioOutput.dispose();
|
||||
mAudioOutput = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this assignment is currently disconnected from a channel source
|
||||
*/
|
||||
public boolean isDisconnected()
|
||||
{
|
||||
return mAudioChannelId == DISCONNECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this assignment is currently connected to a channel source
|
||||
*/
|
||||
public boolean isConnected()
|
||||
{
|
||||
return !isDisconnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects this assignment to the indicated source so that audio
|
||||
* packets from this source can be sent to the audio output
|
||||
*/
|
||||
public void connect(int source, int priority)
|
||||
{
|
||||
mAudioChannelId = source;
|
||||
mPriority = priority;
|
||||
updateLowestPriorityAssignment();
|
||||
|
||||
mAudioOutput.updateTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Currently connected source or -1 if disconnected
|
||||
*/
|
||||
public int getAudioChannelId()
|
||||
{
|
||||
return mAudioChannelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects this assignment from the source and prevents any audio
|
||||
* from being routed to the audio output until another source is assigned
|
||||
*/
|
||||
public void disconnect()
|
||||
{
|
||||
mAudioChannelId = DISCONNECTED;
|
||||
mPriority = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if audio output is current inactive, meaning that the
|
||||
* audio output hasn't recently processed any audio packets.
|
||||
*/
|
||||
public boolean isInactive()
|
||||
{
|
||||
return (mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT) < System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public int getPriority()
|
||||
{
|
||||
return mPriority;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
@@ -34,23 +34,21 @@ public class MonoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 8000;
|
||||
|
||||
public MonoAudioOutput(Mixer mixer)
|
||||
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
|
||||
{
|
||||
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE);
|
||||
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the audio packet data into mono audio frames.
|
||||
*/
|
||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
||||
protected ByteBuffer convert(float[] samples)
|
||||
{
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(packet.hasAudioSamples())
|
||||
if(samples.length > 0)
|
||||
{
|
||||
float[] samples = packet.getAudioSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
@@ -62,8 +60,6 @@ public class MonoAudioOutput extends AudioOutput
|
||||
}
|
||||
}
|
||||
|
||||
packet.decrementUserCount();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
package io.github.dsheirer.audio.playback;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.source.mixer.MixerChannel;
|
||||
|
||||
import javax.sound.sampled.Mixer;
|
||||
@@ -34,10 +34,10 @@ public class StereoAudioOutput extends AudioOutput
|
||||
{
|
||||
private final static int BUFFER_SIZE = 16000;
|
||||
|
||||
public StereoAudioOutput(Mixer mixer, MixerChannel channel)
|
||||
public StereoAudioOutput(Mixer mixer, MixerChannel channel, UserPreferences userPreferences)
|
||||
{
|
||||
super(mixer, channel, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO,
|
||||
BUFFER_SIZE);
|
||||
BUFFER_SIZE, userPreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,14 +45,12 @@ public class StereoAudioOutput extends AudioOutput
|
||||
* channel containing the audio and the other channel containing zero
|
||||
* valued (silent) samples.
|
||||
*/
|
||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
||||
protected ByteBuffer convert(float[] samples)
|
||||
{
|
||||
ByteBuffer buffer = null;
|
||||
|
||||
if(packet.hasAudioSamples())
|
||||
if(samples.length > 0)
|
||||
{
|
||||
float[] samples = packet.getAudioSamples();
|
||||
|
||||
/* Little-endian byte buffer */
|
||||
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
@@ -76,8 +74,6 @@ public class StereoAudioOutput extends AudioOutput
|
||||
}
|
||||
}
|
||||
|
||||
packet.decrementUserCount();
|
||||
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
*/
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
@@ -94,8 +100,8 @@ public class MultiSyncPatternMatcher
|
||||
|
||||
if(mBitCount > mSyncLossThreshold)
|
||||
{
|
||||
mSyncDetectListener.syncLost(mBitCount);
|
||||
mBitCount = 0;
|
||||
mSyncDetectListener.syncLost();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -261,7 +261,12 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
@Override
|
||||
public void receive(IdentifierUpdateNotification update)
|
||||
{
|
||||
// mLog.debug("Received update: " + update + " class:" + update.getIdentifier().getClass());
|
||||
// mLog.debug("Received update: " + update +
|
||||
// " class:" + update.getIdentifier().getIdentifierClass() +
|
||||
// " form:" + update.getIdentifier().getForm() +
|
||||
// " role:" + update.getIdentifier().getRole() +
|
||||
// " class:" + update.getIdentifier().getClass());
|
||||
|
||||
Identifier identifier = update.getIdentifier();
|
||||
|
||||
switch(identifier.getIdentifierClass())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -199,7 +199,8 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
|
||||
}
|
||||
else
|
||||
{
|
||||
return id.getValue() + " TS:" + channelMetadata.getTimeslot();
|
||||
String value = id.getValue().replace(" TS0", "").replace(" TS1", "");
|
||||
return value + " TS:" + channelMetadata.getTimeslot();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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.channel.state;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
|
||||
import io.github.dsheirer.identifier.string.StringIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -35,6 +39,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
|
||||
private static final String NO_SQUELCH = "No Squelch";
|
||||
private Identifier mChannelNameIdentifier;
|
||||
|
||||
private DecoderType mDecoderType;
|
||||
private String mChannelName;
|
||||
@@ -42,7 +47,8 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName)
|
||||
{
|
||||
mDecoderType = decoderType;
|
||||
mChannelName = channelName;
|
||||
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : decoderType.name() + " CHANNEL";
|
||||
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.USER, Form.CHANNEL_NAME, Role.TO);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,9 +80,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
{
|
||||
if(event.getEvent() == Event.RESET)
|
||||
{
|
||||
//Each time we're reset, set the PRIMARY TO attribute back to the channel name, otherwise we won't have
|
||||
//a primary ID for any audio produced by this state.
|
||||
// broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,8 +93,8 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
// broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
||||
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -105,7 +105,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
//Set the proxy as a listener so that echo'd updates are broadcast externally
|
||||
mutableIdentifierCollection.setIdentifierUpdateListener(mIdentifierUpdateNotificationProxy);
|
||||
|
||||
StateMachine stateMachine = new StateMachine(timeslot);
|
||||
StateMachine stateMachine = new StateMachine(timeslot, State.MULTI_CHANNEL_ACTIVE_STATES);
|
||||
mStateMachineMap.put(timeslot, stateMachine);
|
||||
stateMachine.addListener(this);
|
||||
|
||||
@@ -133,7 +133,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
public void stateChanged(State state, int timeslot)
|
||||
{
|
||||
//Broadcast current channel state so that channel rotation monitor can track
|
||||
if(state.isActiveState())
|
||||
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state))
|
||||
{
|
||||
broadcast(DecoderStateEvent.activeState(timeslot));
|
||||
}
|
||||
@@ -148,9 +148,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
|
||||
switch(state)
|
||||
{
|
||||
case IDLE:
|
||||
broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE));
|
||||
break;
|
||||
case RESET:
|
||||
reset(timeslot);
|
||||
mStateMachineMap.get(timeslot).setState(State.IDLE);
|
||||
@@ -173,19 +170,49 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
*/
|
||||
private void checkTeardown()
|
||||
{
|
||||
boolean teardown = true;
|
||||
boolean teardown = false;
|
||||
boolean active = false;
|
||||
|
||||
for(StateMachine stateMachine: mStateMachineMap.values())
|
||||
{
|
||||
if(stateMachine.getState().isActiveState())
|
||||
State state = stateMachine.getState();
|
||||
|
||||
//If we have an active state in either timeslot, don't teardown. IDLE is a special state that is active
|
||||
//but doesn't prevent a teardown when the other timeslot is in TEARDOWN
|
||||
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state) && state != State.IDLE)
|
||||
{
|
||||
teardown = false;
|
||||
active = true;
|
||||
}
|
||||
else if(state == State.TEARDOWN)
|
||||
{
|
||||
teardown = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(teardown)
|
||||
if(teardown && !active)
|
||||
{
|
||||
broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
|
||||
if(mChannel.isTrafficChannel())
|
||||
{
|
||||
broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
|
||||
}
|
||||
else
|
||||
{
|
||||
for(StateMachine stateMachine: mStateMachineMap.values())
|
||||
{
|
||||
stateMachine.setState(State.RESET);
|
||||
}
|
||||
}
|
||||
}
|
||||
//If one timeslot is teardown but the other is still active, reset the teardown timeslot to IDLE
|
||||
else if(teardown && active)
|
||||
{
|
||||
for(StateMachine stateMachine: mStateMachineMap.values())
|
||||
{
|
||||
if(stateMachine.getState() == State.TEARDOWN)
|
||||
{
|
||||
stateMachine.setState(State.RESET);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,11 +367,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
|
||||
{
|
||||
mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers();
|
||||
|
||||
if(mChannel.getChannelType() == ChannelType.TRAFFIC)
|
||||
{
|
||||
mStateMachineMap.get(timeslot).setState(State.ACTIVE);
|
||||
}
|
||||
mStateMachineMap.get(timeslot).setState(State.RESET);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +499,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
//in an active (ie sync locked) state.
|
||||
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
|
||||
{
|
||||
if(mStateMachineMap.get(timeslot).getState().isActiveState())
|
||||
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState()))
|
||||
{
|
||||
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
|
||||
mChannel.getChannelType().name()));
|
||||
@@ -511,7 +534,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
case CONTINUATION:
|
||||
case DECODE:
|
||||
case START:
|
||||
if(State.CALL_STATES.contains(event.getState()))
|
||||
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(event.getState()))
|
||||
{
|
||||
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
|
||||
}
|
||||
@@ -519,11 +542,11 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
case END:
|
||||
if(mChannel.isTrafficChannel())
|
||||
{
|
||||
mStateMachineMap.get(event.getTimeslot()).setState(State.TEARDOWN);
|
||||
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
|
||||
}
|
||||
else
|
||||
{
|
||||
mStateMachineMap.get(event.getTimeslot()).setState(State.FADE);
|
||||
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
|
||||
}
|
||||
break;
|
||||
case RESET:
|
||||
|
||||
@@ -94,7 +94,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
|
||||
private SourceEventListener mInternalSourceEventListener;
|
||||
private ChannelMetadata mChannelMetadata;
|
||||
private StateMachine mStateMachine = new StateMachine(0);
|
||||
private StateMachine mStateMachine = new StateMachine(0, State.SINGLE_CHANNEL_ACTIVE_STATES);
|
||||
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
|
||||
|
||||
public SingleChannelState(Channel channel, AliasModel aliasModel)
|
||||
@@ -123,7 +123,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
public void stateChanged(State state, int timeslot)
|
||||
{
|
||||
//Broadcast current channel state so that channel rotation monitor can track
|
||||
if(state.isActiveState())
|
||||
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(state))
|
||||
{
|
||||
broadcast(DecoderStateEvent.activeState(timeslot));
|
||||
}
|
||||
@@ -377,7 +377,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||
//Rebroadcast frequency error measurements to external listener if we're currently
|
||||
//in an active (ie sync locked) state.
|
||||
if(mStateMachine.getState().isActiveState())
|
||||
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState()))
|
||||
{
|
||||
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
|
||||
mChannel.getChannelType().name()));
|
||||
@@ -411,7 +411,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
case CONTINUATION:
|
||||
case DECODE:
|
||||
case START:
|
||||
if(State.CALL_STATES.contains(event.getState()))
|
||||
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(event.getState()))
|
||||
{
|
||||
mStateMachine.setState(event.getState());
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public enum State
|
||||
state == DATA ||
|
||||
state == ENCRYPTED ||
|
||||
state == FADE ||
|
||||
state == IDLE ||
|
||||
state == TEARDOWN ||
|
||||
state == RESET;
|
||||
}
|
||||
@@ -60,6 +61,7 @@ public enum State
|
||||
state == DATA ||
|
||||
state == ENCRYPTED ||
|
||||
state == FADE ||
|
||||
state == IDLE ||
|
||||
state == TEARDOWN ||
|
||||
state == RESET;
|
||||
}
|
||||
@@ -121,7 +123,8 @@ public enum State
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Indicates the channel is idle and not actively decoding any messages
|
||||
* Indicates the channel is idle and not actively decoding any messages or in a multiple timeslot decoder this
|
||||
* state reflects active decoding that indicates the timeslot is idle.
|
||||
*/
|
||||
IDLE("IDLE")
|
||||
{
|
||||
@@ -159,8 +162,8 @@ public enum State
|
||||
|
||||
private String mDisplayValue;
|
||||
|
||||
public static final EnumSet<State> CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
|
||||
public static final EnumSet<State> IDLE_STATES = EnumSet.of(IDLE, FADE);
|
||||
public static final EnumSet<State> SINGLE_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
|
||||
public static final EnumSet<State> MULTI_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED, IDLE);
|
||||
|
||||
private State(String displayValue)
|
||||
{
|
||||
@@ -169,17 +172,6 @@ public enum State
|
||||
|
||||
public abstract boolean canChangeTo(State state);
|
||||
|
||||
/**
|
||||
* Indicates that this state is an active (ie decoding) state and is one of the states
|
||||
* enumerated in the CALL_STATES enumeration set.
|
||||
*
|
||||
* @return true if this state is an active state
|
||||
*/
|
||||
public boolean isActiveState()
|
||||
{
|
||||
return CALL_STATES.contains(this);
|
||||
}
|
||||
|
||||
public String getDisplayValue()
|
||||
{
|
||||
return mDisplayValue;
|
||||
|
||||
@@ -30,8 +30,12 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* State machine for tracking a channel state.
|
||||
*/
|
||||
public class StateMachine
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(StateMachine.class);
|
||||
@@ -42,13 +46,21 @@ public class StateMachine
|
||||
protected long mEndTimeout;
|
||||
protected long mEndTimeoutBuffer = 0;
|
||||
private int mTimeslot;
|
||||
private EnumSet<State> mActiveStates;
|
||||
private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD;
|
||||
private List<IStateMachineListener> mStateMachineListeners = new ArrayList<>();
|
||||
private Listener<IdentifierUpdateNotification> mIdentifierUpdateListener;
|
||||
|
||||
public StateMachine(int timeslot)
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param timeslot for this state machine
|
||||
* @param activeStates set of states considered active for updating the fade timeout
|
||||
*/
|
||||
public StateMachine(int timeslot, EnumSet<State> activeStates)
|
||||
{
|
||||
mTimeslot = timeslot;
|
||||
mActiveStates = activeStates;
|
||||
}
|
||||
|
||||
public void addListener(IStateMachineListener listener)
|
||||
@@ -73,7 +85,7 @@ public class StateMachine
|
||||
|
||||
public void checkState()
|
||||
{
|
||||
if(mState.isActiveState() && mFadeTimeout <= System.currentTimeMillis())
|
||||
if(mActiveStates.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
|
||||
{
|
||||
setState(State.FADE);
|
||||
}
|
||||
@@ -92,18 +104,22 @@ public class StateMachine
|
||||
{
|
||||
if(state == mState)
|
||||
{
|
||||
if(State.CALL_STATES.contains(state))
|
||||
if(mActiveStates.contains(state))
|
||||
{
|
||||
updateFadeTimeout();
|
||||
}
|
||||
}
|
||||
else if(mState.canChangeTo(state))
|
||||
{
|
||||
if(mActiveStates.contains(state))
|
||||
{
|
||||
updateFadeTimeout();
|
||||
}
|
||||
|
||||
switch(state)
|
||||
{
|
||||
case ACTIVE:
|
||||
mState = state;
|
||||
updateFadeTimeout();
|
||||
broadcast(ChannelStateIdentifier.ACTIVE);
|
||||
break;
|
||||
case CONTROL:
|
||||
@@ -111,23 +127,19 @@ public class StateMachine
|
||||
if(mChannelType == Channel.ChannelType.STANDARD)
|
||||
{
|
||||
mState = state;
|
||||
updateFadeTimeout();
|
||||
broadcast(ChannelStateIdentifier.CONTROL);
|
||||
}
|
||||
break;
|
||||
case DATA:
|
||||
mState = state;
|
||||
updateFadeTimeout();
|
||||
broadcast(ChannelStateIdentifier.DATA);
|
||||
break;
|
||||
case ENCRYPTED:
|
||||
mState = state;
|
||||
updateFadeTimeout();
|
||||
broadcast(ChannelStateIdentifier.ENCRYPTED);
|
||||
break;
|
||||
case CALL:
|
||||
mState = state;
|
||||
updateFadeTimeout();
|
||||
broadcast(ChannelStateIdentifier.CALL);
|
||||
break;
|
||||
case FADE:
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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.controller;
|
||||
|
||||
@@ -37,7 +34,6 @@ import io.github.dsheirer.icon.IconManager;
|
||||
import io.github.dsheirer.map.MapPanel;
|
||||
import io.github.dsheirer.map.MapService;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderManager;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.SourceManager;
|
||||
import io.github.dsheirer.source.tuner.TunerModel;
|
||||
@@ -67,8 +63,7 @@ public class ControllerPanel extends JPanel
|
||||
public ControllerPanel(AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel, BroadcastModel broadcastModel,
|
||||
ChannelModel channelModel, ChannelMapModel channelMapModel, ChannelProcessingManager channelProcessingManager,
|
||||
IconManager iconManager, MapService mapService, SettingsManager settingsManager,
|
||||
SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences,
|
||||
RecorderManager recorderManager)
|
||||
SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
mChannelModel = channelModel;
|
||||
@@ -88,7 +83,7 @@ public class ControllerPanel extends JPanel
|
||||
|
||||
mAliasController = new AliasController(aliasModel, broadcastModel, iconManager, userPreferences);
|
||||
|
||||
mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences, recorderManager);
|
||||
mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -22,6 +22,7 @@
|
||||
package io.github.dsheirer.controller.channel;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadata;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
|
||||
@@ -42,11 +43,10 @@ import io.github.dsheirer.module.decode.event.MessageActivityModel;
|
||||
import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.RecorderFactory;
|
||||
import io.github.dsheirer.record.RecorderManager;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
||||
import io.github.dsheirer.source.Source;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceException;
|
||||
import io.github.dsheirer.source.SourceManager;
|
||||
import org.slf4j.Logger;
|
||||
@@ -70,14 +70,13 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
|
||||
private Map<Channel,ProcessingChain> mProcessingChains = new HashMap<>();
|
||||
|
||||
private List<Listener<ReusableAudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
|
||||
private List<Listener<AudioSegment>> mAudioSegmentListeners = new CopyOnWriteArrayList<>();
|
||||
private List<Listener<IDecodeEvent>> mDecodeEventListeners = new CopyOnWriteArrayList<>();
|
||||
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
|
||||
|
||||
private ChannelMapModel mChannelMapModel;
|
||||
private ChannelMetadataModel mChannelMetadataModel;
|
||||
private EventLogManager mEventLogManager;
|
||||
private RecorderManager mRecorderManager;
|
||||
private SourceManager mSourceManager;
|
||||
private AliasModel mAliasModel;
|
||||
private UserPreferences mUserPreferences;
|
||||
@@ -87,18 +86,15 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
*
|
||||
* @param channelMapModel containing channel maps defined by the user
|
||||
* @param eventLogManager for adding event loggers to channels
|
||||
* @param recorderManager for receiving audio packets produced by the channel
|
||||
* @param sourceManager for obtaining a tuner channel source for the channel
|
||||
* @param aliasModel for aliasing of identifiers produced by the channel
|
||||
* @param userPreferences for user defined behavior and settings
|
||||
*/
|
||||
public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager,
|
||||
RecorderManager recorderManager, SourceManager sourceManager,
|
||||
AliasModel aliasModel, UserPreferences userPreferences)
|
||||
SourceManager sourceManager, AliasModel aliasModel, UserPreferences userPreferences)
|
||||
{
|
||||
mChannelMapModel = channelMapModel;
|
||||
mEventLogManager = eventLogManager;
|
||||
mRecorderManager = recorderManager;
|
||||
mSourceManager = sourceManager;
|
||||
mAliasModel = aliasModel;
|
||||
mUserPreferences = userPreferences;
|
||||
@@ -252,9 +248,9 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
mChannelEventBroadcaster.addListener(processingChain);
|
||||
|
||||
/* Register global listeners */
|
||||
for(Listener<ReusableAudioPacket> listener : mAudioPacketListeners)
|
||||
for(Listener<AudioSegment> listener : mAudioSegmentListeners)
|
||||
{
|
||||
processingChain.addAudioPacketListener(listener);
|
||||
processingChain.addAudioSegmentListener(listener);
|
||||
}
|
||||
|
||||
for(Listener<IDecodeEvent> listener : mDecodeEventListeners)
|
||||
@@ -262,6 +258,30 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
processingChain.addDecodeEventListener(listener);
|
||||
}
|
||||
|
||||
//Add a listener to detect source error state that indicates the channel should be shutdown
|
||||
processingChain.addSourceEventListener(sourceEvent ->
|
||||
{
|
||||
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE && sourceEvent.getSource() != null)
|
||||
{
|
||||
Channel toShutdown = null;
|
||||
|
||||
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChains.entrySet())
|
||||
{
|
||||
if(entry.getValue().hasSource(sourceEvent.getSource()))
|
||||
{
|
||||
toShutdown = entry.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(toShutdown != null)
|
||||
{
|
||||
mLog.info("Channel source error detected - stopping channel [" + toShutdown.getName() + "]");
|
||||
stopProcessing(toShutdown, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//Register this manager to receive channel events from traffic channel manager modules within
|
||||
//the processing chain
|
||||
processingChain.addChannelEventListener(this);
|
||||
@@ -289,7 +309,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
}
|
||||
|
||||
//Add recorders
|
||||
processingChain.addModules(RecorderFactory.getRecorders(mRecorderManager, mUserPreferences, channel));
|
||||
processingChain.addModules(RecorderFactory.getRecorders(mUserPreferences, channel));
|
||||
|
||||
//Set the samples source
|
||||
processingChain.setSource(source);
|
||||
@@ -398,13 +418,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
*/
|
||||
public void shutdown()
|
||||
{
|
||||
mLog.debug("Stopping Channels ...");
|
||||
|
||||
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
|
||||
|
||||
for(Channel channel : channelsToStop)
|
||||
{
|
||||
mLog.debug("Stopping channel: " + channel.toString());
|
||||
stopProcessing(channel, true);
|
||||
}
|
||||
}
|
||||
@@ -413,17 +430,17 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
* Adds a message listener that will be added to all channels to receive
|
||||
* any messages.
|
||||
*/
|
||||
public void addAudioPacketListener(Listener<ReusableAudioPacket> listener)
|
||||
public void addAudioSegmentListener(Listener<AudioSegment> listener)
|
||||
{
|
||||
mAudioPacketListeners.add(listener);
|
||||
mAudioSegmentListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a message listener.
|
||||
*/
|
||||
public void removeAudioPacketListener(Listener<ReusableAudioPacket> listener)
|
||||
public void removeAudioSegmentListener(Listener<AudioSegment> listener)
|
||||
{
|
||||
mAudioPacketListeners.remove(listener);
|
||||
mAudioSegmentListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -62,7 +63,7 @@ import java.util.List;
|
||||
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
|
||||
|
||||
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0");
|
||||
private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 25000;
|
||||
|
||||
//Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers
|
||||
@@ -154,7 +155,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
channels--;
|
||||
}
|
||||
|
||||
mLog.debug("Sample Rate [" + sampleRate + "] providing [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] Hz each");
|
||||
mLog.info("Sample Rate [" + DECIMAL_FORMAT.format(sampleRate) + "] providing [" + channels +
|
||||
"] channels at [" + DECIMAL_FORMAT.format(sampleRate / (double)channels) + "] Hz each");
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -126,6 +126,18 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals to all provisioned tuner channel sources that the source complex buffer provider has an error and can
|
||||
* no longer provide channels, so that the tuner channel source can notify the consumer of the error state.
|
||||
*/
|
||||
public void setErrorMessage(String errorMessage)
|
||||
{
|
||||
for(TunerChannelSource tunerChannelSource: mChannelSources)
|
||||
{
|
||||
tunerChannelSource.setError(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Current channel sample rate which is (2 * channel bandwidth).
|
||||
*/
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 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.fsk;
|
||||
|
||||
@@ -139,7 +141,7 @@ public class LTRDecoder implements Listener<ReusableFloatBuffer>, ISyncDetectLis
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 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.fsk;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
@@ -129,7 +135,7 @@ public class SynchronizationMonitor implements ISyncDetectListener
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Monitors decode sync events to adaptively control PLL loop gain.
|
||||
*/
|
||||
public class AdaptivePLLBandwidthMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLBandwidthMonitor.class);
|
||||
|
||||
private static final int MAX_SYNC_COUNT = PLLBandwidth.BW_200.getRangeEnd();
|
||||
private CostasLoop mCostasLoop;
|
||||
private FeedbackDecoder mFeedbackDecoder;
|
||||
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
|
||||
private int mSyncCount;
|
||||
|
||||
/**
|
||||
* Constructs an adaptive monitor to monitor the sync state of a decoder and adjust the gain
|
||||
* level of the costas loop accordingly.
|
||||
*
|
||||
* @param costasLoop to receive adaptive gain updates.
|
||||
* @param feedbackDecoder to rebroadcast frequency error source events
|
||||
*/
|
||||
public AdaptivePLLBandwidthMonitor(CostasLoop costasLoop, FeedbackDecoder feedbackDecoder)
|
||||
{
|
||||
mCostasLoop = costasLoop;
|
||||
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
|
||||
mCostasLoop.setFrequencyErrorProcessor(this);
|
||||
mFeedbackDecoder = feedbackDecoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync detection event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncDetected(int bitErrors)
|
||||
{
|
||||
mSyncCount++;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync loss event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
mSyncCount -= 2;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the gain state for a sync detect or sync loss
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
if(mSyncCount < 0)
|
||||
{
|
||||
mSyncCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(mSyncCount > MAX_SYNC_COUNT)
|
||||
{
|
||||
mSyncCount = MAX_SYNC_COUNT;
|
||||
return;
|
||||
}
|
||||
|
||||
PLLBandwidth gain = PLLBandwidth.fromSyncCount(mSyncCount);
|
||||
|
||||
//Only update the costas loop if the gain value changes
|
||||
if(gain != mPLLBandwidth)
|
||||
{
|
||||
mPLLBandwidth = gain;
|
||||
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the monitor and commands the PLL to use the highest gain level.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mSyncCount = 0;
|
||||
mPLLBandwidth = PLLBandwidth.BW_400;
|
||||
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFrequencyError(long frequencyError)
|
||||
{
|
||||
//Only rebroadcast the costas loop frequency error if the gain level has progressed above
|
||||
//a bandwidth of 300, meaning that we have received at least 4 sync events
|
||||
if(mPLLBandwidth != PLLBandwidth.BW_400 && mPLLBandwidth != PLLBandwidth.BW_300)
|
||||
{
|
||||
mFeedbackDecoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.P25P1Decoder;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Monitors decode sync events to adaptively control PLL loop gain.
|
||||
*/
|
||||
public class AdaptivePLLGainMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLGainMonitor.class);
|
||||
|
||||
private static final int MAX_SYNC_COUNT = 6;
|
||||
private CostasLoop mCostasLoop;
|
||||
private P25P1Decoder mP25P1Decoder;
|
||||
private PLLGain mPLLGain = PLLGain.LEVEL_1;
|
||||
private int mSyncCount;
|
||||
|
||||
/**
|
||||
* Constructs an adaptive monitor to monitor the sync state of a decoder and adjust the gain
|
||||
* level of the costas loop accordingly.
|
||||
*
|
||||
* @param costasLoop to receive adaptive gain updates.
|
||||
* @param p25P1Decoder to receive frequency error source events
|
||||
*/
|
||||
public AdaptivePLLGainMonitor(CostasLoop costasLoop, P25P1Decoder p25P1Decoder)
|
||||
{
|
||||
mCostasLoop = costasLoop;
|
||||
mCostasLoop.setPLLGain(mPLLGain);
|
||||
mCostasLoop.setFrequencyErrorProcessor(this);
|
||||
mP25P1Decoder = p25P1Decoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync detection event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncDetected(int bitErrors)
|
||||
{
|
||||
mSyncCount++;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync loss event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncLost()
|
||||
{
|
||||
mSyncCount -= 2;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the gain state for a sync detect or sync loss
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
if(mSyncCount < 0)
|
||||
{
|
||||
mSyncCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(mSyncCount > MAX_SYNC_COUNT)
|
||||
{
|
||||
mSyncCount = MAX_SYNC_COUNT;
|
||||
return;
|
||||
}
|
||||
|
||||
PLLGain gain = PLLGain.fromSyncCount(mSyncCount);
|
||||
|
||||
//Only update the costas loop if the gain value changes
|
||||
if(gain != mPLLGain)
|
||||
{
|
||||
mPLLGain = gain;
|
||||
mCostasLoop.setPLLGain(mPLLGain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the monitor and commands the PLL to use the highest gain level.
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mSyncCount = 0;
|
||||
mPLLGain = PLLGain.LEVEL_1;
|
||||
mCostasLoop.setPLLGain(mPLLGain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFrequencyError(long frequencyError)
|
||||
{
|
||||
//Only rebroadcast the costas loop frequency error if the gain level has progressed above the
|
||||
//default gain of 1, meaning that we have received at least 2 sync events
|
||||
if(mPLLGain != PLLGain.LEVEL_1)
|
||||
{
|
||||
mP25P1Decoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
@@ -41,7 +47,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
private double mDamping = Math.sqrt(2.0) / 2.0;
|
||||
private double mAlphaGain;
|
||||
private double mBetaGain;
|
||||
private PLLGain mPLLGain = PLLGain.LEVEL_1;
|
||||
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
|
||||
private double mSymbolRate;
|
||||
private double mSampleRate;
|
||||
private IFrequencyErrorProcessor mFrequencyErrorProcessor;
|
||||
@@ -63,8 +69,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive the measured frequency error as tracked by the PLL at one
|
||||
* second intervals.
|
||||
* Registers the listener to receive the measured frequency error as tracked by the PLL once per second
|
||||
*
|
||||
* @param listener to receive frequency error measurement.
|
||||
*/
|
||||
@@ -102,22 +107,25 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
*/
|
||||
private void updateLoopBandwidth()
|
||||
{
|
||||
double bandwidth = TWO_PI / mPLLGain.getLoopBandwidth();
|
||||
double bandwidth = TWO_PI / mPLLBandwidth.getLoopBandwidth();
|
||||
|
||||
mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
|
||||
mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the PLLGain state for this costas loop. The PLLGain state affects the aggressiveness of the alpha/beta
|
||||
* Sets the PLLBandwidth state for this costas loop. The bandwidth affects the aggressiveness of the alpha/beta
|
||||
* gain values in synchronizing with the signal carrier.
|
||||
*
|
||||
* @param PLLGain
|
||||
* @param pllBandwidth
|
||||
*/
|
||||
public void setPLLGain(PLLGain PLLGain)
|
||||
public void setPLLBandwidth(PLLBandwidth pllBandwidth)
|
||||
{
|
||||
mPLLGain = PLLGain;
|
||||
updateLoopBandwidth();
|
||||
if(mPLLBandwidth != pllBandwidth)
|
||||
{
|
||||
mPLLBandwidth = pllBandwidth;
|
||||
updateLoopBandwidth();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,8 +170,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the costas loop frequency and phase to adjust for the phase
|
||||
* error value
|
||||
* Updates the costas loop frequency and phase to adjust for the phase error value
|
||||
*
|
||||
* @param phaseError - (-)= late and (+)= early
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.module.decode.FeedbackDecoder;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Monitors decode sync events to adaptively control frequency correction broadcasts.
|
||||
*/
|
||||
public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(FrequencyCorrectionSyncMonitor.class);
|
||||
|
||||
private static final int MAX_SYNC_COUNT = PLLBandwidth.BW_200.getRangeEnd();
|
||||
private FeedbackDecoder mFeedbackDecoder;
|
||||
private CostasLoop mCostasLoop;
|
||||
private int mSyncCount;
|
||||
|
||||
/**
|
||||
* Constructs an adaptive monitor to monitor the sync state of a decoder
|
||||
*
|
||||
* @param costasLoop to receive adaptive gain updates.
|
||||
* @param feedbackDecoder to rebroadcast frequency error source events
|
||||
*/
|
||||
public FrequencyCorrectionSyncMonitor(CostasLoop costasLoop, FeedbackDecoder feedbackDecoder)
|
||||
{
|
||||
mCostasLoop = costasLoop;
|
||||
mCostasLoop.setFrequencyErrorProcessor(this);
|
||||
mFeedbackDecoder = feedbackDecoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync detection event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncDetected(int bitErrors)
|
||||
{
|
||||
mSyncCount++;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync loss event. Updates the running sync count and updates the PLL gain level.
|
||||
*/
|
||||
@Override
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
mSyncCount -= 2;
|
||||
update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sync count
|
||||
*/
|
||||
private void update()
|
||||
{
|
||||
if(mSyncCount < 0)
|
||||
{
|
||||
mSyncCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(mSyncCount > MAX_SYNC_COUNT)
|
||||
{
|
||||
mSyncCount = MAX_SYNC_COUNT;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the monitor
|
||||
*/
|
||||
public void reset()
|
||||
{
|
||||
mSyncCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processFrequencyError(long frequencyError)
|
||||
{
|
||||
//Only rebroadcast the frequency error if the sync count is more than 2
|
||||
if(mSyncCount > 2)
|
||||
{
|
||||
mFeedbackDecoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -21,37 +21,45 @@
|
||||
*/
|
||||
package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/**
|
||||
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
|
||||
*/
|
||||
public enum PLLGain
|
||||
public enum PLLBandwidth
|
||||
{
|
||||
//NOTE: static gain level of 200 produced the best results for releases prior to 0.3.4b2
|
||||
//NOTE: static bandwidth of 200 produced the best results for P25P1 releases prior to 0.3.4b2
|
||||
|
||||
LEVEL_0(100.0, 0, 0),
|
||||
LEVEL_1(150.0, 0, 1),
|
||||
LEVEL_2(170.0, 2, 4),
|
||||
LEVEL_3(190.0, 5, 6),
|
||||
LEVEL_4(200.0, 7, 8),
|
||||
LEVEL_5(200.0, 9, 10),
|
||||
LEVEL_6(250.0, 11, 12),
|
||||
LEVEL_7(275.0, 12,13),
|
||||
LEVEL_8(325.0, 14, 15),
|
||||
LEVEL_9(350.0, 16,17),
|
||||
LEVEL_10(375.0, 18, 19),
|
||||
LEVEL_11(400.0, 19,20);
|
||||
BW_400(400.0, 0, 1),
|
||||
BW_300(300.0, 2, 3),
|
||||
BW_250(250.0, 4, 5),
|
||||
BW_200(200.0, 6, 7);
|
||||
|
||||
private double mLoopBandwidth;
|
||||
private int mRangeStart;
|
||||
private int mRangeEnd;
|
||||
|
||||
PLLGain(double loopBandwidth, int start, int end)
|
||||
PLLBandwidth(double loopBandwidth, int start, int end)
|
||||
{
|
||||
mLoopBandwidth = loopBandwidth;
|
||||
mRangeStart = start;
|
||||
mRangeEnd = end;
|
||||
}
|
||||
|
||||
private static Map<Integer,PLLBandwidth> LOOKUP_MAP = new TreeMap<>();
|
||||
|
||||
static
|
||||
{
|
||||
for(PLLBandwidth pllBandwidth : PLLBandwidth.values())
|
||||
{
|
||||
for(int x = pllBandwidth.getRangeStart(); x <= pllBandwidth.getRangeEnd(); x++)
|
||||
{
|
||||
LOOKUP_MAP.put(x, pllBandwidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop bandwidth setting
|
||||
*/
|
||||
@@ -76,27 +84,13 @@ public enum PLLGain
|
||||
* @param syncCount 0-10
|
||||
* @return PLL gain appropriate for the sync detection value
|
||||
*/
|
||||
public static PLLGain fromSyncCount(int syncCount)
|
||||
public static PLLBandwidth fromSyncCount(int syncCount)
|
||||
{
|
||||
if(syncCount <= LEVEL_1.getRangeEnd())
|
||||
if(LOOKUP_MAP.containsKey(syncCount))
|
||||
{
|
||||
return LEVEL_1;
|
||||
}
|
||||
else if(syncCount <= LEVEL_2.getRangeEnd())
|
||||
{
|
||||
return LEVEL_2;
|
||||
}
|
||||
else if(syncCount <= LEVEL_3.getRangeEnd())
|
||||
{
|
||||
return LEVEL_3;
|
||||
}
|
||||
else if(syncCount <= LEVEL_4.getRangeEnd())
|
||||
{
|
||||
return LEVEL_4;
|
||||
}
|
||||
else
|
||||
{
|
||||
return LEVEL_5;
|
||||
return LOOKUP_MAP.get(syncCount);
|
||||
}
|
||||
|
||||
return BW_200;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.psk.pll;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.Dibit;
|
||||
|
||||
/**
|
||||
* Utility for calculating phase inverted sync pattern derivatives for QPSK constellations
|
||||
*/
|
||||
public class QPSKPhaseInversionCalculator
|
||||
{
|
||||
public static void main(String[] args)
|
||||
{
|
||||
|
||||
//DMR sync patterns
|
||||
// String[] syncs = {"755FD7DF75F7", "DFF57D75DF5D", "7F7D5DD57DFD", "D5D7F77FD757", "77D55F7DFD77",
|
||||
// "5D577F7757FF", "F7FDD5DDFD55", "7DFFD5F55D5F", "D7557F5FF7F5", "DD7FF5D757DD"};
|
||||
|
||||
//P25 Phase 2 sync pattern
|
||||
String[] syncs = {"575D57F7FF"};
|
||||
|
||||
//P25 Phase 1 sync pattern
|
||||
// String[] syncs = {"5575F5FF77FF"};
|
||||
|
||||
|
||||
for(String sync: syncs)
|
||||
{
|
||||
StringBuilder sbNorm = new StringBuilder();
|
||||
StringBuilder sbP90 = new StringBuilder();
|
||||
StringBuilder sbM90 = new StringBuilder();
|
||||
StringBuilder sb180 = new StringBuilder();
|
||||
|
||||
for(char letter: sync.toCharArray())
|
||||
{
|
||||
int value;
|
||||
|
||||
if(Character.isAlphabetic(letter))
|
||||
{
|
||||
value = letter - 55;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = letter - 48;
|
||||
}
|
||||
|
||||
sbNorm.append(letter);
|
||||
sbP90.append(rotate(value, Dibit.Rotation.PLUS90));
|
||||
sbM90.append(rotate(value, Dibit.Rotation.MINUS90));
|
||||
sb180.append(rotate(value, Dibit.Rotation.INVERT));
|
||||
}
|
||||
|
||||
System.out.println("NORMAL: " + sync);
|
||||
System.out.println(" +90: " + sbP90.toString());
|
||||
System.out.println(" -90: " + sbM90.toString());
|
||||
System.out.println(" 180: " + sb180.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the hexadecimal value to an integer, decomposes it into high and low order dibits, rotates the dibits
|
||||
* in the specified direction, and recombines the rotated dibits into a 4-bit value, returned as a hex character.
|
||||
* @param value to rotate
|
||||
* @param direction direction
|
||||
* @return rotated hex character
|
||||
*/
|
||||
public static String rotate(int value, Dibit.Rotation direction)
|
||||
{
|
||||
try
|
||||
{
|
||||
Dibit dibitHigh = getHighDibit(value);
|
||||
Dibit dibitLow = getLowDibit(value);
|
||||
return getHex(dibitHigh.rotate(direction), dibitLow.rotate(direction));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
//ignore
|
||||
}
|
||||
|
||||
return "-";
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the bit values from the two dibits and converts the integer value to a hex character
|
||||
* @param d1 containing the high order bits
|
||||
* @param d2 containing the low order bits
|
||||
* @return string hex character representing the value of these dibits combined
|
||||
*/
|
||||
public static String getHex(Dibit d1, Dibit d2)
|
||||
{
|
||||
int value = d1.getHighValue() + d2.getLowValue();
|
||||
return Integer.toHexString(value).toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dibit that represents the bits in the 0x8 and 0x4 bit positions of the value
|
||||
* @param value
|
||||
* @return a dibit
|
||||
*/
|
||||
public static Dibit getHighDibit(int value)
|
||||
{
|
||||
return Dibit.fromValue((value >> 2) & 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the dibit that represents the bits in the 0x2 and 0x1 bit positions of the value
|
||||
* @param value
|
||||
* @return a dibit
|
||||
*/
|
||||
public static Dibit getLowDibit(int value)
|
||||
{
|
||||
return Dibit.fromValue(value & 3);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,25 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.symbol;
|
||||
|
||||
public enum Dibit
|
||||
@@ -133,6 +155,89 @@ public enum Dibit
|
||||
return Dibit.D00_PLUS_1;
|
||||
}
|
||||
|
||||
public static enum Rotation {PLUS90, MINUS90, INVERT}
|
||||
|
||||
/**
|
||||
* Returns the value of this dibit if the symbol were rotated in the direction indicated.
|
||||
*
|
||||
* This can be used for calculating phase rotation and phase inversion pattern derivatives from a normal sync pattern
|
||||
* @param rotation to apply to this dibit
|
||||
* @return rotated version of this dibit
|
||||
*/
|
||||
public Dibit rotate(Rotation rotation)
|
||||
{
|
||||
switch(this)
|
||||
{
|
||||
case D00_PLUS_1:
|
||||
switch(rotation)
|
||||
{
|
||||
case PLUS90:
|
||||
return Dibit.D01_PLUS_3;
|
||||
case MINUS90:
|
||||
return Dibit.D10_MINUS_1;
|
||||
case INVERT:
|
||||
return Dibit.D11_MINUS_3;
|
||||
}
|
||||
break;
|
||||
case D01_PLUS_3:
|
||||
switch(rotation)
|
||||
{
|
||||
case PLUS90:
|
||||
return Dibit.D11_MINUS_3;
|
||||
case MINUS90:
|
||||
return Dibit.D00_PLUS_1;
|
||||
case INVERT:
|
||||
return Dibit.D10_MINUS_1;
|
||||
}
|
||||
break;
|
||||
case D10_MINUS_1:
|
||||
switch(rotation)
|
||||
{
|
||||
case PLUS90:
|
||||
return Dibit.D00_PLUS_1;
|
||||
case MINUS90:
|
||||
return Dibit.D11_MINUS_3;
|
||||
case INVERT:
|
||||
return Dibit.D01_PLUS_3;
|
||||
}
|
||||
break;
|
||||
case D11_MINUS_3:
|
||||
switch(rotation)
|
||||
{
|
||||
case PLUS90:
|
||||
return Dibit.D10_MINUS_1;
|
||||
case MINUS90:
|
||||
return Dibit.D01_PLUS_3;
|
||||
case INVERT:
|
||||
return Dibit.D00_PLUS_1;
|
||||
}
|
||||
}
|
||||
|
||||
//We should never reach this point
|
||||
return Dibit.D00_PLUS_1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dibit that corresponds to the value
|
||||
* @param value 0-3
|
||||
* @return representative dibit or Dibit.00 if the value is outside of the range 0-3
|
||||
*/
|
||||
public static Dibit fromValue(int value)
|
||||
{
|
||||
switch(value)
|
||||
{
|
||||
case 0:
|
||||
default:
|
||||
return Dibit.D00_PLUS_1;
|
||||
case 1:
|
||||
return Dibit.D01_PLUS_3;
|
||||
case 2:
|
||||
return Dibit.D10_MINUS_1;
|
||||
case 3:
|
||||
return Dibit.D11_MINUS_3;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
byte a = (byte)0x1B;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 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
|
||||
@@ -29,7 +29,10 @@ public enum FrameSync
|
||||
P25_PHASE1_ERROR_90_CW( 0x001050551155l ),
|
||||
P25_PHASE1_ERROR_180( 0xAA8A0A008800l ),
|
||||
|
||||
P25_PHASE2_NORMAL(0x575D57F7FFl);
|
||||
P25_PHASE2_NORMAL( 0x575D57F7FFl),
|
||||
P25_PHASE2_ERROR_90_CCW(0xFEFBFEAEAAl),
|
||||
P25_PHASE2_ERROR_90_CW( 0x0104015155l),
|
||||
P25_PHASE2_ERROR_180( 0xA8A2A80800l);
|
||||
|
||||
private long mSync;
|
||||
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
*/
|
||||
package io.github.dsheirer.dsp.symbol;
|
||||
|
||||
/**
|
||||
@@ -31,6 +37,8 @@ public interface ISyncDetectListener
|
||||
|
||||
/**
|
||||
* Indicates that sync has been lost
|
||||
*
|
||||
* @param bitsProcessed number of bits that were processed without a sync detection
|
||||
*/
|
||||
void syncLost();
|
||||
void syncLost(int bitsProcessed);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public class JavaFxWindowManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all JavaFX windows and shutsdown the FX thread
|
||||
* Closes all JavaFX windows and shuts down the FX thread
|
||||
*/
|
||||
public void shutdown()
|
||||
{
|
||||
@@ -84,6 +84,8 @@ public class JavaFxWindowManager
|
||||
{
|
||||
Stage stage = mPreferencesEditor.getStage();
|
||||
stage.show();
|
||||
stage.toFront();
|
||||
stage.requestFocus();
|
||||
mPreferencesEditor.showEditor(request);
|
||||
}
|
||||
catch(Throwable t)
|
||||
|
||||
@@ -1,30 +1,28 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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.gui;
|
||||
|
||||
import com.jidesoft.plaf.LookAndFeelFactory;
|
||||
import com.jidesoft.swing.JideSplitPane;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.AudioPacketManager;
|
||||
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
||||
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
|
||||
@@ -45,7 +43,7 @@ import io.github.dsheirer.module.log.EventLogManager;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.record.RecorderManager;
|
||||
import io.github.dsheirer.record.AudioRecordingManager;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.settings.SettingsManager;
|
||||
import io.github.dsheirer.source.SourceManager;
|
||||
@@ -60,6 +58,7 @@ import io.github.dsheirer.spectrum.SpectralDisplayPanel;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.javafx.IconFontFX;
|
||||
import jiconfont.swing.IconFontSwing;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
@@ -108,7 +107,8 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||
|
||||
private boolean mBroadcastStatusVisible;
|
||||
private AudioPacketManager mAudioPacketManager;
|
||||
private AudioRecordingManager mAudioRecordingManager;
|
||||
private AudioStreamingManager mAudioStreamingManager;
|
||||
private IconManager mIconManager;
|
||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||
private BroadcastModel mBroadcastModel;
|
||||
@@ -164,6 +164,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
|
||||
//Register FontAwesome so we can use the fonts in Swing windows
|
||||
IconFontSwing.register(FontAwesome.getIconFont());
|
||||
IconFontFX.register(FontAwesome.getIconFont());
|
||||
|
||||
TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
|
||||
TunerModel tunerModel = new TunerModel(tunerConfigurationModel);
|
||||
@@ -178,16 +179,12 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
|
||||
ChannelMapModel channelMapModel = new ChannelMapModel();
|
||||
|
||||
EventLogManager eventLogManager = new EventLogManager(mUserPreferences);
|
||||
|
||||
RecorderManager recorderManager = new RecorderManager(aliasModel, mUserPreferences);
|
||||
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
|
||||
|
||||
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences);
|
||||
|
||||
mSourceManager = new SourceManager(tunerModel, mSettingsManager, mUserPreferences);
|
||||
|
||||
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, recorderManager,
|
||||
mSourceManager, aliasModel, mUserPreferences);
|
||||
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, mSourceManager,
|
||||
aliasModel, mUserPreferences);
|
||||
|
||||
mChannelModel.addListener(mChannelProcessingManager);
|
||||
mChannelProcessingManager.addChannelEventListener(mChannelModel);
|
||||
@@ -195,25 +192,26 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
ChannelSelectionManager channelSelectionManager = new ChannelSelectionManager(mChannelModel);
|
||||
mChannelModel.addListener(channelSelectionManager);
|
||||
|
||||
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mSourceManager.getMixerManager());
|
||||
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mUserPreferences);
|
||||
|
||||
mAudioRecordingManager = new AudioRecordingManager(mUserPreferences);
|
||||
mAudioRecordingManager.start();
|
||||
|
||||
mBroadcastModel = new BroadcastModel(aliasModel, mIconManager, mUserPreferences);
|
||||
mAudioStreamingManager = new AudioStreamingManager(mBroadcastModel, BroadcastFormat.MP3,
|
||||
mUserPreferences);
|
||||
mAudioStreamingManager.start();
|
||||
|
||||
//Audio packets are routed through the audio packet manager for metadata enrichment and then
|
||||
//distributed to the audio packet processors (ie playback, recording, streaming, etc.)
|
||||
mAudioPacketManager = new AudioPacketManager(aliasModel);
|
||||
mAudioPacketManager.addListener(recorderManager);
|
||||
mAudioPacketManager.addListener(audioPlaybackManager);
|
||||
mAudioPacketManager.addListener(mBroadcastModel);
|
||||
mAudioPacketManager.start();
|
||||
mChannelProcessingManager.addAudioPacketListener(mAudioPacketManager);
|
||||
mChannelProcessingManager.addAudioSegmentListener(audioPlaybackManager);
|
||||
mChannelProcessingManager.addAudioSegmentListener(mAudioRecordingManager);
|
||||
mChannelProcessingManager.addAudioSegmentListener(mAudioStreamingManager);
|
||||
|
||||
MapService mapService = new MapService(mIconManager);
|
||||
mChannelProcessingManager.addDecodeEventListener(mapService);
|
||||
|
||||
mControllerPanel = new ControllerPanel(audioPlaybackManager, aliasModel, mBroadcastModel,
|
||||
mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager,
|
||||
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences, recorderManager);
|
||||
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences);
|
||||
|
||||
mSpectralPanel = new SpectralDisplayPanel(mChannelModel,
|
||||
mChannelProcessingManager, mSettingsManager, tunerModel);
|
||||
@@ -411,7 +409,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
viewMenu.add(new JSeparator());
|
||||
JMenuItem preferencesItem = new JMenuItem("Preferences");
|
||||
preferencesItem.addActionListener(e -> MyEventBus.getEventBus()
|
||||
.post(new PreferenceEditorViewRequest(PreferenceEditorType.CHANNEL_EVENT)));
|
||||
.post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK)));
|
||||
viewMenu.add(preferencesItem);
|
||||
|
||||
menuBar.add(viewMenu);
|
||||
@@ -471,10 +469,10 @@ public class SDRTrunk implements Listener<TunerEvent>
|
||||
mJavaFxWindowManager.shutdown();
|
||||
mLog.info("Stopping channels ...");
|
||||
mChannelProcessingManager.shutdown();
|
||||
mAudioPacketManager.stop();
|
||||
mAudioRecordingManager.stop();
|
||||
|
||||
mLog.info("Stopping spectral display ...");
|
||||
mSpectralPanel.clearTuner();
|
||||
mLog.info("Releasing tuners ...");
|
||||
mSourceManager.shutdown();
|
||||
mLog.info("Shutdown complete.");
|
||||
mApplicationLog.stop();
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
*/
|
||||
package io.github.dsheirer.gui.instrument.decoder;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
@@ -46,7 +52,7 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
@@ -60,7 +66,7 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
*/
|
||||
package io.github.dsheirer.gui.instrument.decoder;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
@@ -46,7 +52,7 @@ public class MDC1200Pane extends AbstractAFSK1200Pane
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
mLog.debug("Sync Lost!");
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
*/
|
||||
package io.github.dsheirer.gui.instrument.decoder;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
@@ -46,7 +52,7 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
@@ -60,7 +66,7 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* * ******************************************************************************
|
||||
* * 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 distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
*/
|
||||
package io.github.dsheirer.gui.instrument.decoder;
|
||||
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
@@ -46,7 +52,7 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -60,7 +66,7 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost()
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 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
|
||||
@@ -15,13 +14,15 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.preference;
|
||||
|
||||
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
|
||||
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
@@ -36,18 +37,22 @@ public class PreferenceEditorFactory
|
||||
{
|
||||
switch(preferenceEditorType)
|
||||
{
|
||||
case AUDIO_PLAYBACK:
|
||||
return new PlaybackPreferenceEditor(userPreferences);
|
||||
case AUDIO_RECORD:
|
||||
return new RecordPreferenceEditor(userPreferences);
|
||||
case CHANNEL_EVENT:
|
||||
return new DecodeEventViewPreferenceEditor(userPreferences);
|
||||
case JMBE_LIBRARY:
|
||||
return new JmbeLibraryPreferenceEditor(userPreferences);
|
||||
case DIRECTORY:
|
||||
return new DirectoryPreferenceEditor(userPreferences);
|
||||
case JMBE_LIBRARY:
|
||||
return new JmbeLibraryPreferenceEditor(userPreferences);
|
||||
case SOURCE_CHANNEL_MULTIPLE_FREQUENCY:
|
||||
return new ChannelMultipleFrequencyPreferenceEditor(userPreferences);
|
||||
case TALKGROUP_FORMAT:
|
||||
return new TalkgroupFormatPreferenceEditor(userPreferences);
|
||||
case SOURCE_TUNER_CHANNELIZER:
|
||||
return new TunerPreferenceEditor(userPreferences);
|
||||
case TALKGROUP_FORMAT:
|
||||
return new TalkgroupFormatPreferenceEditor(userPreferences);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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.gui.preference;
|
||||
@@ -28,11 +25,14 @@ package io.github.dsheirer.gui.preference;
|
||||
public enum PreferenceEditorType
|
||||
{
|
||||
CHANNEL_EVENT("Channel Events"),
|
||||
JMBE_LIBRARY("JMBE Audio Library"),
|
||||
DIRECTORY("Directories"),
|
||||
JMBE_LIBRARY("JMBE Audio Library"),
|
||||
AUDIO_RECORD("Record"),
|
||||
AUDIO_PLAYBACK("Playback"),
|
||||
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
|
||||
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
|
||||
TALKGROUP_FORMAT("Talkgroup & Radio ID");
|
||||
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
|
||||
DEFAULT("Default");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 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
|
||||
@@ -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.gui.preference;
|
||||
@@ -29,12 +28,13 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.control.TreeView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
@@ -52,25 +52,19 @@ public class PreferencesEditor extends Application
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PreferencesEditor.class);
|
||||
|
||||
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
|
||||
private UserPreferences mUserPreferences;
|
||||
private BorderPane mBorderPane;
|
||||
private HBox mParentBox;
|
||||
private TreeView mEditorSelectionTreeView;
|
||||
private DecodeEventViewPreferenceEditor mDecodeEventViewPreferenceEditor;
|
||||
private VBox mDefaultEditor;
|
||||
private HBox mControlBox;
|
||||
|
||||
/**
|
||||
* Constructs a preferences editor instance
|
||||
*/
|
||||
public PreferencesEditor()
|
||||
{
|
||||
}
|
||||
private VBox mEditorAndButtonsBox;
|
||||
private Node mEditor;
|
||||
private HBox mButtonsBox;
|
||||
|
||||
public Stage getStage()
|
||||
{
|
||||
try
|
||||
{
|
||||
Window window = getBorderPane().getScene().getWindow();
|
||||
Window window = getParentBox().getScene().getWindow();
|
||||
|
||||
if(window instanceof Stage)
|
||||
{
|
||||
@@ -104,7 +98,7 @@ public class PreferencesEditor extends Application
|
||||
public void start(Stage stage) throws Exception
|
||||
{
|
||||
stage.setTitle("Preferences");
|
||||
Scene scene = new Scene(getBorderPane(), 900, 500);
|
||||
Scene scene = new Scene(getParentBox(), 900, 500);
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
}
|
||||
@@ -130,8 +124,7 @@ public class PreferencesEditor extends Application
|
||||
{
|
||||
TreeItem treeItem = li.next();
|
||||
|
||||
if(treeItem.getValue() instanceof PreferenceEditorType &&
|
||||
((PreferenceEditorType)treeItem.getValue()).equals(type))
|
||||
if(treeItem.getValue() instanceof PreferenceEditorType && (treeItem.getValue()).equals(type))
|
||||
{
|
||||
return treeItem;
|
||||
}
|
||||
@@ -153,31 +146,48 @@ public class PreferencesEditor extends Application
|
||||
/**
|
||||
* Primary layout for the editor window
|
||||
*/
|
||||
private BorderPane getBorderPane()
|
||||
private HBox getParentBox()
|
||||
{
|
||||
if(mBorderPane == null)
|
||||
if(mParentBox == null)
|
||||
{
|
||||
mBorderPane = new BorderPane();
|
||||
mBorderPane.setLeft(getEditorSelectionTreeView());
|
||||
mBorderPane.setCenter(getDefaultEditor());
|
||||
mBorderPane.setBottom(getControlBox());
|
||||
mParentBox = new HBox();
|
||||
mParentBox.getChildren().add(getEditorSelectionTreeView());
|
||||
HBox.setHgrow(getEditorAndButtonsBox(), Priority.ALWAYS);
|
||||
mParentBox.getChildren().add(getEditorAndButtonsBox());
|
||||
}
|
||||
|
||||
return mBorderPane;
|
||||
return mParentBox;
|
||||
}
|
||||
|
||||
private VBox getDefaultEditor()
|
||||
private VBox getEditorAndButtonsBox()
|
||||
{
|
||||
if(mDefaultEditor == null)
|
||||
if(mEditorAndButtonsBox == null)
|
||||
{
|
||||
mDefaultEditor = new VBox();
|
||||
mDefaultEditor.setPadding(new Insets(10, 10, 10, 10));
|
||||
Label label = new Label("Please select a preference ...");
|
||||
|
||||
mDefaultEditor.getChildren().add(label);
|
||||
mEditorAndButtonsBox = new VBox();
|
||||
mEditor = getDefaultEditor();
|
||||
VBox.setVgrow(getDefaultEditor(), Priority.ALWAYS);
|
||||
VBox.setVgrow(getButtonsBox(), Priority.NEVER);
|
||||
mEditorAndButtonsBox.getChildren().addAll(getDefaultEditor(), getButtonsBox());
|
||||
}
|
||||
|
||||
return mDefaultEditor;
|
||||
return mEditorAndButtonsBox;
|
||||
}
|
||||
|
||||
private Node getDefaultEditor()
|
||||
{
|
||||
Node editor = mEditors.get(PreferenceEditorType.DEFAULT);
|
||||
|
||||
if(editor == null)
|
||||
{
|
||||
VBox defaultEditor = new VBox();
|
||||
defaultEditor.setPadding(new Insets(10, 10, 10, 10));
|
||||
Label label = new Label("Please select a preference ...");
|
||||
defaultEditor.getChildren().add(label);
|
||||
mEditors.put(PreferenceEditorType.DEFAULT, defaultEditor);
|
||||
editor = defaultEditor;
|
||||
}
|
||||
|
||||
return editor;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,6 +199,12 @@ public class PreferencesEditor extends Application
|
||||
{
|
||||
TreeItem<String> treeRoot = new TreeItem<>("Root node");
|
||||
|
||||
TreeItem<String> audioItem = new TreeItem<>("Audio");
|
||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_PLAYBACK));
|
||||
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
|
||||
treeRoot.getChildren().add(audioItem);
|
||||
audioItem.setExpanded(true);
|
||||
|
||||
TreeItem<String> decoderItem = new TreeItem<>("Decoder");
|
||||
decoderItem.getChildren().add(new TreeItem(PreferenceEditorType.JMBE_LIBRARY));
|
||||
treeRoot.getChildren().add(decoderItem);
|
||||
@@ -217,43 +233,56 @@ public class PreferencesEditor extends Application
|
||||
treeRoot.setExpanded(true);
|
||||
mEditorSelectionTreeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
mEditorSelectionTreeView.getSelectionModel().selectedItemProperty().addListener(new EditorTreeSelectionListener());
|
||||
|
||||
mEditorSelectionTreeView.setMinWidth(Control.USE_PREF_SIZE);
|
||||
}
|
||||
|
||||
return mEditorSelectionTreeView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode event view preferences editor
|
||||
*/
|
||||
private DecodeEventViewPreferenceEditor getDecodeEventViewPreferenceEditor()
|
||||
{
|
||||
if(mDecodeEventViewPreferenceEditor == null)
|
||||
{
|
||||
mDecodeEventViewPreferenceEditor = new DecodeEventViewPreferenceEditor(getUserPreferences());
|
||||
}
|
||||
|
||||
return mDecodeEventViewPreferenceEditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control box with OK button.
|
||||
*/
|
||||
private HBox getControlBox()
|
||||
private HBox getButtonsBox()
|
||||
{
|
||||
if(mControlBox == null)
|
||||
if(mButtonsBox == null)
|
||||
{
|
||||
mControlBox = new HBox();
|
||||
mButtonsBox = new HBox();
|
||||
mButtonsBox.setMaxWidth(Double.MAX_VALUE);
|
||||
Button okButton = new Button("Ok");
|
||||
okButton.setOnAction(event -> {
|
||||
Stage stage = (Stage)getBorderPane().getScene().getWindow();
|
||||
Stage stage = (Stage)getParentBox().getScene().getWindow();
|
||||
stage.close();
|
||||
});
|
||||
HBox.setMargin(okButton, new Insets(5, 5, 5, 5));
|
||||
mControlBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
mControlBox.getChildren().add(okButton);
|
||||
mButtonsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
mButtonsBox.getChildren().add(okButton);
|
||||
}
|
||||
|
||||
return mControlBox;
|
||||
return mButtonsBox;
|
||||
}
|
||||
|
||||
private void setEditor(PreferenceEditorType type)
|
||||
{
|
||||
Node editor = mEditors.get(type);
|
||||
|
||||
if(editor == null)
|
||||
{
|
||||
if(type == PreferenceEditorType.DEFAULT)
|
||||
{
|
||||
editor = getDefaultEditor();
|
||||
}
|
||||
else
|
||||
{
|
||||
editor = PreferenceEditorFactory.getEditor(type, getUserPreferences());
|
||||
mEditors.put(type, editor);
|
||||
}
|
||||
}
|
||||
|
||||
getEditorAndButtonsBox().getChildren().remove(mEditor);
|
||||
VBox.setVgrow(editor, Priority.ALWAYS);
|
||||
mEditor = editor;
|
||||
getEditorAndButtonsBox().getChildren().add(0, mEditor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,44 +292,22 @@ public class PreferencesEditor extends Application
|
||||
*/
|
||||
public class EditorTreeSelectionListener implements ChangeListener
|
||||
{
|
||||
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue observable, Object oldValue, Object newValue)
|
||||
{
|
||||
Node editor = getEditor(newValue);
|
||||
BorderPane.setAlignment(editor, Pos.CENTER_LEFT);
|
||||
getBorderPane().setCenter(editor);
|
||||
}
|
||||
|
||||
private Node getEditor(Object treeNodeItem)
|
||||
{
|
||||
if(treeNodeItem instanceof TreeItem)
|
||||
if(newValue instanceof TreeItem)
|
||||
{
|
||||
Object value = ((TreeItem)treeNodeItem).getValue();
|
||||
Object value = ((TreeItem)newValue).getValue();
|
||||
|
||||
if(value instanceof PreferenceEditorType)
|
||||
{
|
||||
PreferenceEditorType type = (PreferenceEditorType)value;
|
||||
|
||||
Node editor = mEditors.get(type);
|
||||
|
||||
if(editor != null)
|
||||
{
|
||||
return editor;
|
||||
}
|
||||
|
||||
editor = PreferenceEditorFactory.getEditor(type, getUserPreferences());
|
||||
|
||||
if(editor != null)
|
||||
{
|
||||
mEditors.put(type, editor);
|
||||
return editor;
|
||||
}
|
||||
setEditor((PreferenceEditorType)value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return getDefaultEditor();
|
||||
setEditor(PreferenceEditorType.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,433 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.preference.playback;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.playback.PlaybackPreference;
|
||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||
import io.github.dsheirer.source.mixer.MixerManager;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.paint.Color;
|
||||
import jiconfont.icons.font_awesome.FontAwesome;
|
||||
import jiconfont.javafx.IconNode;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioSystem;
|
||||
import javax.sound.sampled.Clip;
|
||||
import javax.sound.sampled.DataLine;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.ShortBuffer;
|
||||
|
||||
|
||||
/**
|
||||
* Preference settings for audio playback
|
||||
*/
|
||||
public class PlaybackPreferenceEditor extends HBox
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PlaybackPreferenceEditor.class);
|
||||
private PlaybackPreference mPlaybackPreference;
|
||||
private GridPane mEditorPane;
|
||||
private ComboBox<MixerChannelConfiguration> mMixerComboBox;
|
||||
private Button mMixerTestButton;
|
||||
private ToggleSwitch mUseAudioSegmentStartToneSwitch;
|
||||
private Button mTestStartToneButton;
|
||||
private ToggleSwitch mUseAudioSegmentPreemptToneSwitch;
|
||||
private Button mTestPreemptToneButton;
|
||||
private ToggleSwitch mUseAudioSegmentDropToneSwitch;
|
||||
private Button mTestDropToneButton;
|
||||
private ComboBox<ToneFrequency> mStartToneFrequencyComboBox;
|
||||
private ComboBox<ToneVolume> mStartToneVolumeComboBox;
|
||||
private ComboBox<ToneFrequency> mPreemptToneFrequencyComboBox;
|
||||
private ComboBox<ToneVolume> mPreemptToneVolumeComboBox;
|
||||
private ComboBox<ToneFrequency> mDropToneFrequencyComboBox;
|
||||
private ComboBox<ToneVolume> mDropToneVolumeComboBox;
|
||||
|
||||
public PlaybackPreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mPlaybackPreference = userPreferences.getPlaybackPreference();
|
||||
|
||||
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
|
||||
getChildren().add(getEditorPane());
|
||||
}
|
||||
|
||||
private GridPane getEditorPane()
|
||||
{
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
int row = 0;
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
mEditorPane.setHgap(10);
|
||||
mEditorPane.setVgap(10);
|
||||
Label outputLabel = new Label("Audio Output Device");
|
||||
GridPane.setHalignment(outputLabel, HPos.RIGHT);
|
||||
mEditorPane.add(outputLabel, 0, row, 2, 1);
|
||||
mEditorPane.add(getMixerComboBox(), 2, row, 3, 1);
|
||||
mEditorPane.add(getMixerTestButton(), 5, row);
|
||||
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, ++row, 6, 1);
|
||||
mEditorPane.add(new Label("Audio Playback Insert Tones"), 0, ++row, 2, 1);
|
||||
|
||||
mEditorPane.add(getUseAudioSegmentStartToneSwitch(), 0, ++row);
|
||||
mEditorPane.add(new Label("Start Tone"), 1, row, 3, 1);
|
||||
Label startFrequencyLabel = new Label("Frequency:");
|
||||
GridPane.setHalignment(startFrequencyLabel, HPos.RIGHT);
|
||||
mEditorPane.add(startFrequencyLabel, 1, ++row);
|
||||
mEditorPane.add(getStartToneFrequencyComboBox(), 2, row);
|
||||
Label startVolumeLabel = new Label("Volume:");
|
||||
GridPane.setHalignment(startVolumeLabel, HPos.RIGHT);
|
||||
mEditorPane.add(startVolumeLabel, 3, row);
|
||||
mEditorPane.add(getStartToneVolumeComboBox(), 4, row);
|
||||
mEditorPane.add(getTestStartToneButton(), 5, row);
|
||||
|
||||
mEditorPane.add(getUseAudioSegmentPreemptToneSwitch(), 0, ++row);
|
||||
mEditorPane.add(new Label("Priority Preempt Tone"), 1, row, 3, 1);
|
||||
Label preemptFrequencyLabel = new Label("Frequency:");
|
||||
GridPane.setHalignment(preemptFrequencyLabel, HPos.RIGHT);
|
||||
mEditorPane.add(preemptFrequencyLabel, 1, ++row);
|
||||
mEditorPane.add(getPreemptToneFrequencyComboBox(), 2, row);
|
||||
Label preemptVolumeLabel = new Label("Volume:");
|
||||
GridPane.setHalignment(preemptVolumeLabel, HPos.RIGHT);
|
||||
mEditorPane.add(preemptVolumeLabel, 3, row);
|
||||
mEditorPane.add(getPreemptToneVolumeComboBox(), 4, row);
|
||||
mEditorPane.add(getTestPreemptToneButton(), 5, row);
|
||||
|
||||
mEditorPane.add(getUseAudioSegmentDropToneSwitch(), 0, ++row);
|
||||
mEditorPane.add(new Label("Drop Tone - Do Not Monitor"), 1, row, 3, 1);
|
||||
Label dropFrequencyLabel = new Label("Frequency:");
|
||||
GridPane.setHalignment(dropFrequencyLabel, HPos.RIGHT);
|
||||
mEditorPane.add(dropFrequencyLabel, 1, ++row);
|
||||
mEditorPane.add(getDropToneFrequencyComboBox(), 2, row);
|
||||
Label dropVolumeLabel = new Label("Volume:");
|
||||
GridPane.setHalignment(dropVolumeLabel, HPos.RIGHT);
|
||||
mEditorPane.add(dropVolumeLabel, 3, row);
|
||||
mEditorPane.add(getDropToneVolumeComboBox(), 4, row);
|
||||
mEditorPane.add(getTestDropToneButton(), 5, row);
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
private ComboBox<MixerChannelConfiguration> getMixerComboBox()
|
||||
{
|
||||
if(mMixerComboBox == null)
|
||||
{
|
||||
mMixerComboBox = new ComboBox<>();
|
||||
mMixerComboBox.getItems().addAll(MixerManager.getOutputMixers());
|
||||
mMixerComboBox.getSelectionModel().select(mPlaybackPreference.getMixerChannelConfiguration());
|
||||
mMixerComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setMixerChannelConfiguration(newValue));
|
||||
}
|
||||
|
||||
return mMixerComboBox;
|
||||
}
|
||||
|
||||
public Button getMixerTestButton()
|
||||
{
|
||||
if(mMixerTestButton == null)
|
||||
{
|
||||
mMixerTestButton = new Button("Test");
|
||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||
iconNode.setFill(Color.CORNFLOWERBLUE);
|
||||
mMixerTestButton.setGraphic(iconNode);
|
||||
mMixerTestButton.setOnAction(event -> play(mPlaybackPreference.getMixerTestTone()));
|
||||
}
|
||||
|
||||
return mMixerTestButton;
|
||||
}
|
||||
|
||||
private ToggleSwitch getUseAudioSegmentStartToneSwitch()
|
||||
{
|
||||
if(mUseAudioSegmentStartToneSwitch == null)
|
||||
{
|
||||
mUseAudioSegmentStartToneSwitch = new ToggleSwitch();
|
||||
mUseAudioSegmentStartToneSwitch.setAlignment(Pos.BASELINE_RIGHT);
|
||||
mUseAudioSegmentStartToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentStartTone());
|
||||
mUseAudioSegmentStartToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
|
||||
{
|
||||
mPlaybackPreference.setUseAudioSegmentStartTone(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mUseAudioSegmentStartToneSwitch;
|
||||
}
|
||||
|
||||
private ToggleSwitch getUseAudioSegmentPreemptToneSwitch()
|
||||
{
|
||||
if(mUseAudioSegmentPreemptToneSwitch == null)
|
||||
{
|
||||
mUseAudioSegmentPreemptToneSwitch = new ToggleSwitch();
|
||||
mUseAudioSegmentPreemptToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentPreemptTone());
|
||||
mUseAudioSegmentPreemptToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
|
||||
{
|
||||
mPlaybackPreference.setUseAudioSegmentPreemptTone(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mUseAudioSegmentPreemptToneSwitch;
|
||||
}
|
||||
|
||||
private ToggleSwitch getUseAudioSegmentDropToneSwitch()
|
||||
{
|
||||
if(mUseAudioSegmentDropToneSwitch == null)
|
||||
{
|
||||
mUseAudioSegmentDropToneSwitch = new ToggleSwitch();
|
||||
mUseAudioSegmentDropToneSwitch.setSelected(mPlaybackPreference.getUseAudioSegmentDropTone());
|
||||
mUseAudioSegmentDropToneSwitch.selectedProperty().addListener(new ChangeListener<Boolean>()
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
|
||||
{
|
||||
mPlaybackPreference.setUseAudioSegmentDropTone(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mUseAudioSegmentDropToneSwitch;
|
||||
}
|
||||
|
||||
public Button getTestStartToneButton()
|
||||
{
|
||||
if(mTestStartToneButton == null)
|
||||
{
|
||||
mTestStartToneButton = new Button("Test");
|
||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||
iconNode.setFill(Color.CORNFLOWERBLUE);
|
||||
mTestStartToneButton.setGraphic(iconNode);
|
||||
mTestStartToneButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
play(mPlaybackPreference.getStartTone());
|
||||
}
|
||||
});
|
||||
|
||||
mTestStartToneButton.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mTestStartToneButton;
|
||||
}
|
||||
|
||||
public Button getTestPreemptToneButton()
|
||||
{
|
||||
if(mTestPreemptToneButton == null)
|
||||
{
|
||||
mTestPreemptToneButton = new Button("Test");
|
||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||
iconNode.setFill(Color.CORNFLOWERBLUE);
|
||||
mTestPreemptToneButton.setGraphic(iconNode);
|
||||
mTestPreemptToneButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
play(mPlaybackPreference.getPreemptTone());
|
||||
}
|
||||
});
|
||||
|
||||
mTestPreemptToneButton.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mTestPreemptToneButton;
|
||||
}
|
||||
|
||||
public Button getTestDropToneButton()
|
||||
{
|
||||
if(mTestDropToneButton == null)
|
||||
{
|
||||
mTestDropToneButton = new Button("Test");
|
||||
IconNode iconNode = new IconNode(FontAwesome.PLAY);
|
||||
iconNode.setFill(Color.CORNFLOWERBLUE);
|
||||
mTestDropToneButton.setGraphic(iconNode);
|
||||
mTestDropToneButton.setOnAction(new EventHandler<ActionEvent>()
|
||||
{
|
||||
@Override
|
||||
public void handle(ActionEvent event)
|
||||
{
|
||||
play(mPlaybackPreference.getDropTone());
|
||||
}
|
||||
});
|
||||
|
||||
mTestDropToneButton.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mTestDropToneButton;
|
||||
}
|
||||
|
||||
public ComboBox<ToneFrequency> getDropToneFrequencyComboBox()
|
||||
{
|
||||
if(mDropToneFrequencyComboBox == null)
|
||||
{
|
||||
mDropToneFrequencyComboBox = new ComboBox<>();
|
||||
mDropToneFrequencyComboBox.getItems().addAll(ToneFrequency.values());
|
||||
mDropToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getDropToneFrequency());
|
||||
mDropToneFrequencyComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setDropToneFrequency(newValue));
|
||||
mDropToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mDropToneFrequencyComboBox;
|
||||
}
|
||||
|
||||
public ComboBox<ToneVolume> getDropToneVolumeComboBox()
|
||||
{
|
||||
if(mDropToneVolumeComboBox == null)
|
||||
{
|
||||
mDropToneVolumeComboBox = new ComboBox<>();
|
||||
mDropToneVolumeComboBox.getItems().addAll(ToneVolume.values());
|
||||
mDropToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getDropToneVolume());
|
||||
mDropToneVolumeComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setDropToneVolume(newValue));
|
||||
mDropToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentDropToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mDropToneVolumeComboBox;
|
||||
}
|
||||
|
||||
public ComboBox<ToneFrequency> getStartToneFrequencyComboBox()
|
||||
{
|
||||
if(mStartToneFrequencyComboBox == null)
|
||||
{
|
||||
mStartToneFrequencyComboBox = new ComboBox<>();
|
||||
mStartToneFrequencyComboBox.getItems().addAll(ToneFrequency.values());
|
||||
mStartToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getStartToneFrequency());
|
||||
mStartToneFrequencyComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setStartToneFrequency(newValue));
|
||||
mStartToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mStartToneFrequencyComboBox;
|
||||
}
|
||||
|
||||
public ComboBox<ToneVolume> getStartToneVolumeComboBox()
|
||||
{
|
||||
if(mStartToneVolumeComboBox == null)
|
||||
{
|
||||
mStartToneVolumeComboBox = new ComboBox<>();
|
||||
mStartToneVolumeComboBox.getItems().addAll(ToneVolume.values());
|
||||
mStartToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getStartToneVolume());
|
||||
mStartToneVolumeComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setStartToneVolume(newValue));
|
||||
mStartToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentStartToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mStartToneVolumeComboBox;
|
||||
}
|
||||
|
||||
public ComboBox<ToneFrequency> getPreemptToneFrequencyComboBox()
|
||||
{
|
||||
if(mPreemptToneFrequencyComboBox == null)
|
||||
{
|
||||
mPreemptToneFrequencyComboBox = new ComboBox<>();
|
||||
mPreemptToneFrequencyComboBox.getItems().addAll(ToneFrequency.values());
|
||||
mPreemptToneFrequencyComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneFrequency());
|
||||
mPreemptToneFrequencyComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneFrequency(newValue));
|
||||
mPreemptToneFrequencyComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mPreemptToneFrequencyComboBox;
|
||||
}
|
||||
|
||||
public ComboBox<ToneVolume> getPreemptToneVolumeComboBox()
|
||||
{
|
||||
if(mPreemptToneVolumeComboBox == null)
|
||||
{
|
||||
mPreemptToneVolumeComboBox = new ComboBox<>();
|
||||
mPreemptToneVolumeComboBox.getItems().addAll(ToneVolume.values());
|
||||
mPreemptToneVolumeComboBox.getSelectionModel().select(mPlaybackPreference.getPreemptToneVolume());
|
||||
mPreemptToneVolumeComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mPlaybackPreference.setPreemptToneVolume(newValue));
|
||||
mPreemptToneVolumeComboBox.disableProperty().bind(getUseAudioSegmentPreemptToneSwitch().selectedProperty().not());
|
||||
}
|
||||
|
||||
return mPreemptToneVolumeComboBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays the audio buffer over the default mono playback device
|
||||
* @param audioSamples with 8 kHz mono PCM samples
|
||||
*/
|
||||
private void play(float[] audioSamples)
|
||||
{
|
||||
if(audioSamples != null)
|
||||
{
|
||||
/* Little-endian byte buffer */
|
||||
ByteBuffer buffer = ByteBuffer.allocate(audioSamples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
ShortBuffer shortBuffer = buffer.asShortBuffer();
|
||||
|
||||
for(float sample : audioSamples)
|
||||
{
|
||||
shortBuffer.put((short) (sample * Short.MAX_VALUE));
|
||||
}
|
||||
|
||||
byte[] bytes = buffer.array();
|
||||
|
||||
DataLine.Info info = new DataLine.Info(Clip.class, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO);
|
||||
|
||||
if(!AudioSystem.isLineSupported(info))
|
||||
{
|
||||
mLog.error("Audio clip playback is not supported on this system");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Clip clip = (Clip)AudioSystem.getLine(info);
|
||||
clip.open(AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO, bytes, 0, bytes.length);
|
||||
clip.start();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error attempting to play audio test tone");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.preference.playback;
|
||||
|
||||
public enum ToneFrequency
|
||||
{
|
||||
F300(300),
|
||||
F400(400),
|
||||
F500(500),
|
||||
F600(600),
|
||||
F700(700),
|
||||
F800(800),
|
||||
F900(900),
|
||||
F1000(1000),
|
||||
F1100(1100),
|
||||
F1200(1200),
|
||||
F1300(1300),
|
||||
F1400(1400),
|
||||
F1500(1500),
|
||||
F1600(1600),
|
||||
F1700(1700),
|
||||
F1800(1800),
|
||||
F1900(1900),
|
||||
F2000(2000),
|
||||
F2100(2100),
|
||||
F2200(2200),
|
||||
F2300(2300),
|
||||
F2400(2400),
|
||||
F2500(2500);
|
||||
|
||||
private int mValue;
|
||||
|
||||
ToneFrequency(int value)
|
||||
{
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.valueOf(mValue);
|
||||
}
|
||||
|
||||
public static ToneFrequency fromValue(int value)
|
||||
{
|
||||
for(ToneFrequency toneFrequency: ToneFrequency.values())
|
||||
{
|
||||
if(toneFrequency.getValue() == value)
|
||||
{
|
||||
return toneFrequency;
|
||||
}
|
||||
}
|
||||
|
||||
return F1200; //default
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.preference.playback;
|
||||
|
||||
import io.github.dsheirer.dsp.mixer.Oscillator;
|
||||
|
||||
/**
|
||||
* Utility for generating tones for audio clips
|
||||
*/
|
||||
public class ToneUtil
|
||||
{
|
||||
//Maximum tone volume: 0.0 <> 1.0
|
||||
private static final double MAX_TONE_VOLUME = 0.5f;
|
||||
|
||||
/**
|
||||
* Generates a tone using the specified parameters
|
||||
* @param toneFrequency of the tone
|
||||
* @param toneVolume of the tone 1-10, to a maximum 90% gain
|
||||
* @param sampleCount number of 8 kHz samples to generate
|
||||
* @return buffer of audio
|
||||
*/
|
||||
public static float[] getTone(ToneFrequency toneFrequency, ToneVolume toneVolume, int sampleCount)
|
||||
{
|
||||
Oscillator oscillator = new Oscillator(toneFrequency.getValue(), 8000.0);
|
||||
|
||||
float[] samples = oscillator.generateReal(sampleCount);
|
||||
|
||||
double gain = MAX_TONE_VOLUME * ((double)toneVolume.getValue() / 10.0);
|
||||
|
||||
for(int x = 0; x < samples.length; x++)
|
||||
{
|
||||
samples[x] *= gain;
|
||||
}
|
||||
|
||||
//Attenuate beginning and end samples
|
||||
if(sampleCount > 10)
|
||||
{
|
||||
for(int x = 0; x < 10; x++)
|
||||
{
|
||||
samples[x] *= (float)x / 10.0f;
|
||||
}
|
||||
|
||||
for(int x = 0; x < 10; x++)
|
||||
{
|
||||
samples[samples.length - 1 - x] *= (float)x / 10.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.preference.playback;
|
||||
|
||||
public enum ToneVolume
|
||||
{
|
||||
V1(1),
|
||||
V2(2),
|
||||
V3(3),
|
||||
V4(4),
|
||||
V5(5),
|
||||
V6(6),
|
||||
V7(7),
|
||||
V8(8),
|
||||
V9(9),
|
||||
V10(10);
|
||||
|
||||
private int mValue;
|
||||
|
||||
ToneVolume(int value)
|
||||
{
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public int getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return String.valueOf(mValue);
|
||||
}
|
||||
|
||||
public static ToneVolume fromValue(int value)
|
||||
{
|
||||
for(ToneVolume toneVolume: ToneVolume.values())
|
||||
{
|
||||
if(toneVolume.getValue() == value)
|
||||
{
|
||||
return toneVolume;
|
||||
}
|
||||
}
|
||||
|
||||
return V3; //default
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.preference.record;
|
||||
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.record.RecordPreference;
|
||||
import io.github.dsheirer.record.RecordFormat;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
/**
|
||||
* Preference settings for recording
|
||||
*/
|
||||
public class RecordPreferenceEditor extends HBox
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(RecordPreferenceEditor.class);
|
||||
private RecordPreference mRecordPreference;
|
||||
private GridPane mEditorPane;
|
||||
private ComboBox<RecordFormat> mRecordFormatComboBox;
|
||||
|
||||
public RecordPreferenceEditor(UserPreferences userPreferences)
|
||||
{
|
||||
mRecordPreference = userPreferences.getRecordPreference();
|
||||
HBox.setHgrow(getEditorPane(), Priority.ALWAYS);
|
||||
getChildren().add(getEditorPane());
|
||||
}
|
||||
|
||||
private GridPane getEditorPane()
|
||||
{
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
mEditorPane.setHgap(10);
|
||||
mEditorPane.setVgap(10);
|
||||
|
||||
Label label = new Label("Audio Recording Format:");
|
||||
mEditorPane.add(label, 0, 0);
|
||||
|
||||
mEditorPane.add(getRecordFormatComboBox(), 1, 0);
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
private ComboBox<RecordFormat> getRecordFormatComboBox()
|
||||
{
|
||||
if(mRecordFormatComboBox == null)
|
||||
{
|
||||
mRecordFormatComboBox = new ComboBox<>();
|
||||
mRecordFormatComboBox.getItems().addAll(RecordFormat.values());
|
||||
mRecordFormatComboBox.getSelectionModel().select(mRecordPreference.getAudioRecordFormat());
|
||||
mRecordFormatComboBox.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<RecordFormat>()
|
||||
{
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends RecordFormat> observable, RecordFormat oldValue, RecordFormat newValue)
|
||||
{
|
||||
mRecordPreference.setAudioRecordFormat(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mRecordFormatComboBox;
|
||||
}
|
||||
}
|
||||
@@ -64,11 +64,11 @@ public class TunerPreferenceEditor extends HBox
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(10);
|
||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||
GridPane.setMargin(getChannelizerLabel(), new Insets(0, 10, 0, 0));
|
||||
GridPane.setHalignment(getChannelizerLabel(), HPos.LEFT);
|
||||
mEditorPane.add(getChannelizerLabel(), 0, 0);
|
||||
GridPane.setMargin(getChannelizerTypeChoiceBox(), new Insets(2, 0, 2, 0));
|
||||
mEditorPane.add(getChannelizerTypeChoiceBox(), 1, 0);
|
||||
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, 1, 2, 1);
|
||||
mEditorPane.add(getPolyphaseLabel(), 0, 2, 2, 1);
|
||||
|
||||
@@ -81,7 +81,7 @@ public class Icon implements Comparable<Icon>
|
||||
{
|
||||
try
|
||||
{
|
||||
if(mPath.startsWith("/"))
|
||||
if(!mPath.startsWith("images"))
|
||||
{
|
||||
mImageIcon = new ImageIcon(mPath);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 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.identifier;
|
||||
@@ -41,7 +38,6 @@ public class IdentifierCollection
|
||||
private final static Logger mLog = LoggerFactory.getLogger(IdentifierCollection.class);
|
||||
protected List<Identifier> mIdentifiers = new ArrayList<>();
|
||||
protected AliasListConfigurationIdentifier mAliasListConfigurationIdentifier;
|
||||
private boolean mUpdated = false;
|
||||
private int mTimeslot = 0;
|
||||
|
||||
/**
|
||||
@@ -94,19 +90,6 @@ public class IdentifierCollection
|
||||
mTimeslot = timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this identifier collection contains an updated list of identifiers.
|
||||
*/
|
||||
public boolean isUpdated()
|
||||
{
|
||||
return mUpdated;
|
||||
}
|
||||
|
||||
public void setUpdated(boolean updated)
|
||||
{
|
||||
mUpdated = updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias List configuration identifier containing the name of the alias list for this collection.
|
||||
* @return alias list or null
|
||||
@@ -319,4 +302,19 @@ public class IdentifierCollection
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Identifier Collection - Timeslot:").append(mTimeslot).append("\n");
|
||||
for(Identifier identifier: getIdentifiers())
|
||||
{
|
||||
sb.append("\t").append(identifier.toString());
|
||||
sb.append("\t{").append(identifier.getIdentifierClass().name()).append("|")
|
||||
.append(identifier.getForm().name()).append("|")
|
||||
.append(identifier.getRole().name()).append("}");
|
||||
sb.append("\t").append(identifier.getClass()).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
|
||||
*/
|
||||
private void notifyAdd(Identifier identifier)
|
||||
{
|
||||
setUpdated(true);
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, getTimeslot()));
|
||||
@@ -110,7 +109,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
|
||||
*/
|
||||
private void notifyRemove(Identifier identifier)
|
||||
{
|
||||
setUpdated(true);
|
||||
if(mListener != null)
|
||||
{
|
||||
mListener.receive(new IdentifierUpdateNotification(identifier,
|
||||
@@ -409,8 +407,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
|
||||
public IdentifierCollection copyOf()
|
||||
{
|
||||
IdentifierCollection copy = new IdentifierCollection(getIdentifiers(), getTimeslot());
|
||||
copy.setUpdated(isUpdated());
|
||||
setUpdated(false);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier.string;
|
||||
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Simple string identifier
|
||||
*/
|
||||
public class SimpleStringIdentifier extends StringIdentifier
|
||||
{
|
||||
public SimpleStringIdentifier(String value, IdentifierClass identifierClass, Form form, Role role)
|
||||
{
|
||||
super(value, identifierClass, form, role);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.UNKNOWN;
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier.tone;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.string.StringListIdentifier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Call Progress (ie dial tone, busy tone) tones identifier
|
||||
*/
|
||||
public abstract class CallProgressIdentifier extends StringListIdentifier
|
||||
{
|
||||
public CallProgressIdentifier(List<String> values)
|
||||
{
|
||||
super(values, IdentifierClass.USER, Form.CALL_PROGRESS_TONE, Role.TO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "CALL:" + Joiner.on("").join(getValue());
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier.tone;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.string.StringListIdentifier;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Knox tones identifier
|
||||
*/
|
||||
public abstract class KnoxIdentifier extends StringListIdentifier
|
||||
{
|
||||
public KnoxIdentifier(List<String> values)
|
||||
{
|
||||
super(values, IdentifierClass.USER, Form.KNOX_TONE, Role.TO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "KNOX:" + Joiner.on("").join(getValue());
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.identifier.tone;
|
||||
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class P25CallProgressIdentifier extends CallProgressIdentifier
|
||||
{
|
||||
public P25CallProgressIdentifier(List<String> values)
|
||||
{
|
||||
super(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return Protocol.APCO25;
|
||||
}
|
||||
|
||||
public static P25CallProgressIdentifier create(List<String> values)
|
||||
{
|
||||
return new P25CallProgressIdentifier(values);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user