mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-13 00:57:31 -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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</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"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<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">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</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
|
0.3.4 20180418
|
||||||
- New P25 C4FM decoder that uses Differential QPSK decoder instead of FM demodulation with 4FSK.
|
- 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
|
- 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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * it under the terms of the GNU General Public License as published by
|
||||||
@@ -23,17 +23,18 @@ plugins {
|
|||||||
id 'application'
|
id 'application'
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'idea'
|
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'
|
id 'org.beryx.runtime' version '1.8.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { url "https://jitpack.io" }
|
||||||
}
|
}
|
||||||
|
|
||||||
version = '0.4.0-beta.2'
|
version = '0.4.0'
|
||||||
sourceCompatibility = '11'
|
sourceCompatibility = '13'
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main.java.srcDirs 'src/main'
|
main.java.srcDirs 'src/main'
|
||||||
@@ -41,45 +42,56 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testCompile 'junit:junit:4.12'
|
testImplementation 'junit:junit:4.12'
|
||||||
testCompile 'org.assertj:assertj-core:3.8.0'
|
testImplementation 'org.assertj:assertj-core:3.8.0'
|
||||||
compile fileTree(dir: 'imports', include: '*.jar')
|
|
||||||
compile 'ch.qos.logback:logback-core:1.2.3'
|
//Jitpack imports
|
||||||
compile 'ch.qos.logback:logback-classic:1.2.3'
|
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||||
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
|
implementation 'com.github.dnault:libresample4j:master'
|
||||||
compile 'com.fazecast:jSerialComm:2.5.0'
|
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
|
||||||
compile 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
|
||||||
compile 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
//mavenCentral/jcenter imports
|
||||||
compile 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||||
compile 'com.github.wendykierp:JTransforms:3.1'
|
implementation 'ch.qos.logback:logback-core:1.2.3'
|
||||||
compile 'com.google.guava:guava:27.0.1-jre'
|
implementation 'ch.qos.logback:logback-classic:1.2.3'
|
||||||
compile 'com.jidesoft:jide-oss:3.6.18'
|
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
|
||||||
compile 'com.miglayout:miglayout-swing:5.2'
|
implementation 'com.fazecast:jSerialComm:2.5.0'
|
||||||
compile 'eu.hansolo:charts:1.0.5'
|
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||||
compile 'io.github.dsheirer:radio-reference-api:15.1.2'
|
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
||||||
compile 'javax.usb:usb-api:1.0.2'
|
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||||
compile 'net.coderazzi:tablefilter-swing:5.4.0'
|
implementation 'com.github.wendykierp:JTransforms:3.1'
|
||||||
compile 'org.apache.commons:commons-lang3:3.8.1'
|
implementation 'com.google.guava:guava:27.0.1-jre'
|
||||||
compile 'org.apache.commons:commons-io:1.3.2'
|
implementation 'com.jidesoft:jide-oss:3.6.18'
|
||||||
compile 'org.apache.mina:mina-core:2.0.19'
|
implementation 'com.miglayout:miglayout-swing:5.2'
|
||||||
compile 'org.apache.mina:mina-http:2.0.19'
|
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||||
compile "org.openjfx:javafx-base:11:win"
|
implementation 'eu.hansolo:charts:1.0.5'
|
||||||
compile "org.openjfx:javafx-base:11:linux"
|
implementation 'io.github.dsheirer:radio-reference-api:15.1.2'
|
||||||
compile "org.openjfx:javafx-base:11:mac"
|
implementation 'javax.usb:usb-api:1.0.2'
|
||||||
compile "org.openjfx:javafx-controls:11.0.2:win"
|
implementation 'net.coderazzi:tablefilter-swing:5.4.0'
|
||||||
compile "org.openjfx:javafx-controls:11.0.2:linux"
|
implementation 'org.apache.commons:commons-lang3:3.8.1'
|
||||||
compile "org.openjfx:javafx-controls:11.0.2:mac"
|
implementation 'org.apache.commons:commons-io:1.3.2'
|
||||||
compile "org.openjfx:javafx-graphics:11.0.2:win"
|
implementation 'org.apache.mina:mina-core:2.0.19'
|
||||||
compile "org.openjfx:javafx-graphics:11.0.2:linux"
|
implementation 'org.apache.mina:mina-http:2.0.19'
|
||||||
compile "org.openjfx:javafx-graphics:11.0.2:mac"
|
implementation 'org.controlsfx:controlsfx:11.0.1'
|
||||||
compile "org.openjfx:javafx-swing:11.0.2:win"
|
implementation 'org.slf4j:slf4j-api:1.7.25'
|
||||||
compile "org.openjfx:javafx-swing:11.0.2:linux"
|
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||||
compile "org.openjfx:javafx-swing:11.0.2:mac"
|
implementation 'org.usb4java:usb4java:1.3.0'
|
||||||
compile 'org.slf4j:slf4j-api:1.7.25'
|
implementation 'org.usb4java:usb4java-javax:1.3.0'
|
||||||
compile 'org.usb4java:libusb4java:1.3.0'
|
implementation 'pl.edu.icm:JLargeArrays:1.6'
|
||||||
compile 'org.usb4java:usb4java:1.3.0'
|
|
||||||
compile 'org.usb4java:usb4java-javax:1.3.0'
|
//The must remain as compile options to ensure they are included in the build to produce multi-OS images
|
||||||
compile 'pl.edu.icm:JLargeArrays:1.6'
|
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 {
|
application {
|
||||||
@@ -100,128 +112,117 @@ javafx {
|
|||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes (
|
attributes (
|
||||||
'Implementation-Title' : 'sdrtrunk project',
|
'Implementation-Title' : 'sdrtrunk project',
|
||||||
'Implementation-Version' : archiveVersion,
|
'Implementation-Version' : archiveVersion,
|
||||||
'Class-Path' : 'jmbe-1.0.1.jar',
|
'Class-Path' : 'jmbe-1.0.4.jar',
|
||||||
'Created-By' : "Gradle ${gradle.gradleVersion}",
|
'Created-By' : "Gradle ${gradle.gradleVersion}",
|
||||||
'Build-Timestamp' : new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
|
'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-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']}"
|
'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
|
* Java Development Kit (JDK) locations. In order to build OS-specific images, these paths must point to the 'bin'
|
||||||
* within a JDK for each of the specified architectures. These JDKs are used by the
|
* directory within a JDK for each of the specified architectures. These JDKs are used by the runtime and runtimeZip
|
||||||
* runtime and runtimeZip tasks to produce platform-specific builds
|
* 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 jdk_linux_x86_64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-14'
|
||||||
def jdkLinux_x86 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-13.0.1'
|
def jdk_linux_x86_32 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-14'
|
||||||
def jdkOsx_x64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-13.0.1.jdk'
|
def jdk_osx_x86_64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-14.jdk'
|
||||||
def jdkWindows_x64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-13.0.1'
|
def jdk_windows_x86_64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-14'
|
||||||
def jdkWindows_x86 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-13.0.1'
|
def jdk_windows_x86_32 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-14'
|
||||||
//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 file_jdk_linux_x86_64 = new File(jdk_linux_x86_64)
|
||||||
//def jfxLinux_arm32 = '/media/denny/WD250GB/java/bellsoft/linux-arm32/armv6hf-sdk/lib'
|
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 {
|
runtime {
|
||||||
targetPlatform('linux-x86_64', jdkLinux_x64)
|
if(file_jdk_linux_x86_64.exists())
|
||||||
targetPlatform('linux-x86_32', jdkLinux_x86)
|
{
|
||||||
targetPlatform('osx-x86_64', jdkOsx_x64)
|
targetPlatform('linux-x86_64-v' + version, jdk_linux_x86_64)
|
||||||
targetPlatform('windows-x86_64', jdkWindows_x64)
|
}
|
||||||
targetPlatform('windows-x86_32', jdkWindows_x86)
|
|
||||||
// targetPlatform('linux-aarch64', jdkLinux_aarch64)
|
if(file_jdk_linux_x86_32.exists())
|
||||||
// targetPlatform('linux-arm32', jdkLinux_arm32)
|
{
|
||||||
|
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']
|
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
|
||||||
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop']
|
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop', 'java.net.http']
|
||||||
imageZip = file("$buildDir/image/sdr-trunk-" + version + ".zip")
|
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
|
* Check for existence of JDK folders for each supported platform before creating runtime images
|
||||||
*/
|
*/
|
||||||
tasks.runtime.doFirst {
|
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)
|
println("Skipping OS Image - Linux x86 64-bit JDK was not found at " + jdk_linux_x86_64)
|
||||||
throw new GradleException("Cannot find Java Development Kit (JDK) for linux-x64 architecture. " +
|
|
||||||
"Please update the build.gradle script to provide the correct path")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def linux_x86 = new File(jdkLinux_x86)
|
if(!file_jdk_linux_x86_32.exists())
|
||||||
|
|
||||||
if(!linux_x86.exists())
|
|
||||||
{
|
{
|
||||||
println("Linux x86 JDK was not found at " + jdkLinux_x86)
|
println("Skipping OS Image - Linux x86 32-bit JDK was not found at " + jdk_linux_x86_32)
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def osx_x64 = new File(jdkOsx_x64)
|
if(!file_jdk_osx_x86_64.exists())
|
||||||
|
|
||||||
if(!osx_x64.exists())
|
|
||||||
{
|
{
|
||||||
println("OSX x64 JDK was not found at " + jdkOsx_x64);
|
println("Skipping OS Image - OSX x86 64-bit JDK was not found at " + jdk_osx_x86_64);
|
||||||
throw new GradleException("Cannot find Java Development Kit (JDK) for osx-x64 architecture. " +
|
|
||||||
"Please update the build.gradle script to provide the correct path")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def windows_x64 = new File(jdkWindows_x64)
|
if(!file_jdk_windows_x86_64.exists())
|
||||||
|
|
||||||
if(!windows_x64.exists())
|
|
||||||
{
|
{
|
||||||
println("Windows x64 JDK was not found at " + jdkWindows_x64)
|
println("Skipping OS Image - Windows x86 64-bit JDK was not found at " + jdk_windows_x86_64)
|
||||||
throw new GradleException("Cannot find Java Development Kit (JDK) for windows-x64 architecture. " +
|
|
||||||
"Please update the build.gradle script to provide the correct path")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def windows_x86 = new File(jdkWindows_x86)
|
if(!file_jdk_windows_x86_32.exists())
|
||||||
|
|
||||||
if(!windows_x86.exists())
|
|
||||||
{
|
{
|
||||||
println("Windows x86 32-bit JDK was not found at " + jdkWindows_x86)
|
println("Skipping OS Image - Windows x86 32-bit JDK was not found at " + jdk_windows_x86_32)
|
||||||
throw new GradleException("Cannot find Java Development Kit (JDK) for windows-x64 architecture. " +
|
|
||||||
"Please update the build.gradle script to provide the correct path")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 unused JavaFX OS-specific libraries from each platform target directory
|
||||||
* remove them from each build image
|
|
||||||
*/
|
*/
|
||||||
tasks.runtime.doLast {
|
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_64-v' + version + '/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-linux-x86_32-v' + version + '/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-osx-x86_64-v' + version + '/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_64-v' + version + '/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-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/ })
|
//JavaFX web and media modules are transitive dependencies of ControlsFX but are not required for this application
|
||||||
// delete(fileTree('build/image/sdr-trunk-linux-arm32/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
|
delete(fileTree('build/image/sdr-trunk-linux-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||||
// copy {
|
delete(fileTree('build/image/sdr-trunk-linux-x86_32-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||||
// from(jfxLinux_arm32).exclude("src.zip")
|
delete(fileTree('build/image/sdr-trunk-osx-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
|
||||||
// into 'build/image/sdr-trunk-linux-arm32/lib'
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
51
gradlew
vendored
51
gradlew
vendored
@@ -1,5 +1,21 @@
|
|||||||
#!/usr/bin/env sh
|
#!/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
|
## Gradle start up script for UN*X
|
||||||
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
|
|||||||
APP_BASE_NAME=`basename "$0"`
|
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.
|
# 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.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
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\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
if $cygwin ; then
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
@@ -138,19 +154,19 @@ if $cygwin ; then
|
|||||||
else
|
else
|
||||||
eval `echo args$i`="\"$arg\""
|
eval `echo args$i`="\"$arg\""
|
||||||
fi
|
fi
|
||||||
i=$((i+1))
|
i=`expr $i + 1`
|
||||||
done
|
done
|
||||||
case $i in
|
case $i in
|
||||||
(0) set -- ;;
|
0) set -- ;;
|
||||||
(1) set -- "$args0" ;;
|
1) set -- "$args0" ;;
|
||||||
(2) set -- "$args0" "$args1" ;;
|
2) set -- "$args0" "$args1" ;;
|
||||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -159,14 +175,9 @@ save () {
|
|||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
echo " "
|
echo " "
|
||||||
}
|
}
|
||||||
APP_ARGS=$(save "$@")
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# 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"
|
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" "$@"
|
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
|
@if "%DEBUG%" == "" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@rem
|
@rem
|
||||||
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
|
|||||||
set APP_HOME=%DIRNAME%
|
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.
|
@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
|
@rem Find java.exe
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
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">
|
<component name="DependencyValidationManager">
|
||||||
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
@@ -60,21 +61,7 @@
|
|||||||
<version value="1.0" />
|
<version value="1.0" />
|
||||||
</component>
|
</component>
|
||||||
<component name="JavadocGenerationManager">
|
<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="OTHER_OPTIONS" value="" />
|
||||||
<option name="HEAP_SIZE" />
|
|
||||||
<option name="LOCALE" />
|
|
||||||
<option name="OPEN_IN_BROWSER" value="true" />
|
|
||||||
</component>
|
</component>
|
||||||
<component name="Palette2">
|
<component name="Palette2">
|
||||||
<group name="Swing">
|
<group name="Swing">
|
||||||
|
|||||||
@@ -538,9 +538,12 @@ public class AliasList implements Listener<AliasEvent>
|
|||||||
{
|
{
|
||||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||||
|
|
||||||
mLog.warn("Alias [" + alias.getName() + "] talkgroup [" + talkgroup.getValue() +
|
if(!existing.equals(alias))
|
||||||
"] has the same talkgroup value as alias [" + existing.getName() +
|
{
|
||||||
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
|
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);
|
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
|
//Log warning if the new talkgroup range overlaps with any existing ranges
|
||||||
for(Map.Entry<TalkgroupRange,Alias> entry: mTalkgroupRangeAliasMap.entrySet())
|
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() +
|
mLog.warn("Alias [" + alias.getName() + "] with talkgroup range [" + talkgroupRange.toString() +
|
||||||
"] overlaps with alias [" + entry.getValue().getName() +
|
"] overlaps with alias [" + entry.getValue().getName() +
|
||||||
@@ -621,9 +624,12 @@ public class AliasList implements Listener<AliasEvent>
|
|||||||
{
|
{
|
||||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||||
|
|
||||||
mLog.warn("Alias [" + alias.getName() + "] radio ID [" + radio.getValue() +
|
if(!existing.equals(alias))
|
||||||
"] has the same value as alias [" + existing.getName() +
|
{
|
||||||
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
|
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);
|
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
|
//Log warning if the new range overlaps with any existing ranges
|
||||||
for(Map.Entry<RadioRange,Alias> entry: mRadioRangeAliasMap.entrySet())
|
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() +
|
mLog.warn("Alias [" + alias.getName() + "] with radio ID range [" + radioRange.toString() +
|
||||||
"] overlaps with alias [" + entry.getValue().getName() +
|
"] overlaps with alias [" + entry.getValue().getName() +
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ public class Radio extends AliasID
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
TalkgroupFormat talkgroupFormat = TalkgroupFormat.get(mProtocol);
|
RadioFormat radioFormat = RadioFormat.get(mProtocol);
|
||||||
return talkgroupFormat.getMinimumValidValue() <= mValue && mValue <= talkgroupFormat.getMaximumValidValue();
|
return radioFormat.getMinimumValidValue() <= mValue && mValue <= radioFormat.getMaximumValidValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toString()
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * 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
|
public enum RadioFormat
|
||||||
{
|
{
|
||||||
APCO25("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
APCO25("********", 0, 0xFFFFFF, "0 to 16,777,215",
|
||||||
"<html>APCO25 valid range is 1 to 16,777,215"),
|
"<html>APCO25 valid range is 0 to 16,777,215"),
|
||||||
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
||||||
"Unknown protocol valid value range is 1-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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* *
|
||||||
* (at your option) any later version.
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.alias.id.talkgroup;
|
||||||
@@ -32,8 +34,8 @@ import io.github.dsheirer.protocol.Protocol;
|
|||||||
*/
|
*/
|
||||||
public enum TalkgroupFormat
|
public enum TalkgroupFormat
|
||||||
{
|
{
|
||||||
APCO25("********", 1, 0xFFFFFF, "1 to 16,777,215",
|
APCO25("********", 0, 0xFFFFFF, "0 to 65,535",
|
||||||
"<html>APCO25 valid range is 1 to 65,535(talkgroup)<br>or 1 to 16,777,215 (radio ID)"),
|
"<html>APCO25 talkgroup valid range is 0 to 65,535"),
|
||||||
FLEETSYNC("###-####", 1, 0x7FFFFF, "001-0001 to 127-8192",
|
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)"),
|
"<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",
|
LTR("##-###", 257, 5375, "01-001 to 20-255",
|
||||||
|
|||||||
@@ -20,41 +20,134 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.audio;
|
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.IdentifierUpdateListener;
|
||||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||||
import io.github.dsheirer.module.Module;
|
import io.github.dsheirer.module.Module;
|
||||||
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
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.
|
* Base audio module implementation.
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractAudioModule extends Module implements IAudioPacketProvider, IdentifierUpdateListener,
|
public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener
|
||||||
ISquelchStateListener
|
|
||||||
{
|
{
|
||||||
//Static unique audio channel identifier.
|
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class);
|
||||||
private static int AUDIO_CHANNEL_ID_GENERATOR = 1;
|
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<AudioSegment> mAudioSegmentListener;
|
||||||
private Listener<ReusableAudioPacket> mAudioPacketListener;
|
protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
|
||||||
private 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
|
* 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
|
* Gets the current audio segment, or creates a new audio segment as necessary and broadcasts it to any registered
|
||||||
* be identified within a combined audio packet stream
|
* 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
|
@Override
|
||||||
public Listener<IdentifierUpdateNotification> getIdentifierUpdateListener()
|
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
|
@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
|
@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;
|
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.SquelchState;
|
||||||
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
|
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
|
||||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
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.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.buffer.IReusableBufferListener;
|
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 io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides packaging of demodulated audio sample buffers into audio packets for broadcast to registered audio packet
|
* Provides packaging of demodulated audio sample buffers into audio segments for broadcast to registered listeners.
|
||||||
* listeners. Includes audio packet metadata in constructed audio packets.
|
* Includes audio packet metadata in constructed audio segments.
|
||||||
*
|
*
|
||||||
* Incorporates audio squelch state listener to control if audio packets are broadcast or ignored.
|
* 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 final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||||
private static float[] sHighPassFilterCoefficients;
|
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 RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients);
|
||||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||||
private boolean mRecordAudioOverride;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Audio Module.
|
* Creates an Audio Module.
|
||||||
*/
|
*/
|
||||||
public AudioModule()
|
public AudioModule(AliasList aliasList)
|
||||||
{
|
{
|
||||||
|
super(aliasList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getTimeslot()
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dispose()
|
public void dispose()
|
||||||
{
|
{
|
||||||
removeAudioPacketListener();
|
removeAudioSegmentListener();
|
||||||
mSquelchStateListener = null;
|
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
|
@Override
|
||||||
public void reset()
|
public void reset()
|
||||||
{
|
{
|
||||||
@@ -114,30 +107,8 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
|
|||||||
@Override
|
@Override
|
||||||
public void start()
|
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
|
@Override
|
||||||
public Listener<SquelchStateEvent> getSquelchStateListener()
|
public Listener<SquelchStateEvent> getSquelchStateListener()
|
||||||
{
|
{
|
||||||
@@ -147,23 +118,11 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
|
|||||||
@Override
|
@Override
|
||||||
public void receive(ReusableFloatBuffer reusableFloatBuffer)
|
public void receive(ReusableFloatBuffer reusableFloatBuffer)
|
||||||
{
|
{
|
||||||
if(hasAudioPacketListener() && mSquelchState == SquelchState.UNSQUELCH)
|
if(mSquelchState == SquelchState.UNSQUELCH)
|
||||||
{
|
{
|
||||||
ReusableFloatBuffer highPassFiltered = mHighPassFilter.filter(reusableFloatBuffer);
|
ReusableFloatBuffer buffer = mHighPassFilter.filter(reusableFloatBuffer);
|
||||||
|
addAudio(buffer.getSamples());
|
||||||
ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount());
|
buffer.decrementUserCount();
|
||||||
audioPacket.resetAttributes();
|
|
||||||
audioPacket.setAudioChannelId(getAudioChannelId());
|
|
||||||
audioPacket.loadAudioFrom(highPassFiltered);
|
|
||||||
if(mRecordAudioOverride)
|
|
||||||
{
|
|
||||||
audioPacket.setRecordable(true);
|
|
||||||
}
|
|
||||||
audioPacket.setIdentifierCollection(getIdentifierCollection().copyOf());
|
|
||||||
|
|
||||||
getAudioPacketListener().receive(audioPacket);
|
|
||||||
|
|
||||||
highPassFiltered.decrementUserCount();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -186,16 +145,12 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
|
|||||||
@Override
|
@Override
|
||||||
public void receive(SquelchStateEvent event)
|
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();
|
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;
|
package io.github.dsheirer.audio;
|
||||||
|
|
||||||
import io.github.dsheirer.sample.ConversionUtils;
|
import io.github.dsheirer.sample.ConversionUtils;
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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
|
* 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();
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
for(ReusableAudioPacket audioPacket: audioPackets)
|
for(float[] audioBuffer: buffers)
|
||||||
{
|
{
|
||||||
//Converting from 32-bit floats to signed 16-bit samples
|
//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());
|
stream.write(buffer.array());
|
||||||
audioPacket.decrementUserCount();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(IOException e)
|
catch(IOException e)
|
||||||
@@ -53,4 +51,29 @@ public class AudioUtils
|
|||||||
|
|
||||||
return stream.toByteArray();
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * 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);
|
mAudioRecordingQueue.offer(recording);
|
||||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
recording.removePendingReplay();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,22 +18,25 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.audio.broadcast;
|
package io.github.dsheirer.audio.broadcast;
|
||||||
|
|
||||||
|
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
public class AudioRecording implements Comparable<AudioRecording>
|
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 Path mPath;
|
||||||
private long mStartTime;
|
private long mStartTime;
|
||||||
private long mRecordingLength;
|
private long mRecordingLength;
|
||||||
private AtomicInteger mPendingReplayCount = new AtomicInteger();
|
private AtomicInteger mPendingReplayCount = new AtomicInteger();
|
||||||
private IdentifierCollection mIdentifierCollection;
|
private IdentifierCollection mIdentifierCollection;
|
||||||
|
private Collection<BroadcastChannel> mBroadcastChannels;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio recording that is ready to be streamed
|
* 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 start time of recording in milliseconds since epoch
|
||||||
* @param recordingLength in milliseconds
|
* @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;
|
mPath = path;
|
||||||
|
mBroadcastChannels = broadcastChannels;
|
||||||
mIdentifierCollection = identifierCollection;
|
mIdentifierCollection = identifierCollection;
|
||||||
mStartTime = start;
|
mStartTime = start;
|
||||||
mRecordingLength = recordingLength;
|
mRecordingLength = recordingLength;
|
||||||
@@ -59,6 +64,14 @@ public class AudioRecording implements Comparable<AudioRecording>
|
|||||||
return mPath;
|
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.
|
* 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.gui.editor.EmptyEditor;
|
||||||
import io.github.dsheirer.icon.IconManager;
|
import io.github.dsheirer.icon.IconManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
public class BroadcastFactory
|
public class BroadcastFactory
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastFactory.class);
|
private final static Logger mLog = LoggerFactory.getLogger(BroadcastFactory.class);
|
||||||
@@ -186,21 +182,6 @@ public class BroadcastFactory
|
|||||||
return editor;
|
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)
|
public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format)
|
||||||
{
|
{
|
||||||
switch(format)
|
switch(format)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.audio.broadcast;
|
package io.github.dsheirer.audio.broadcast;
|
||||||
|
|
||||||
import io.github.dsheirer.alias.AliasList;
|
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||||
import io.github.dsheirer.icon.IconManager;
|
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.properties.SystemProperties;
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -51,7 +49,7 @@ import java.util.function.Consumer;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
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);
|
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,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
|
||||||
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
|
||||||
private IconManager mIconManager;
|
private IconManager mIconManager;
|
||||||
private StreamManager mStreamManager;
|
|
||||||
private AliasModel mAliasModel;
|
private AliasModel mAliasModel;
|
||||||
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
|
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
|
||||||
|
|
||||||
@@ -87,8 +84,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
|
|||||||
{
|
{
|
||||||
mAliasModel = aliasModel;
|
mAliasModel = aliasModel;
|
||||||
mIconManager = iconManager;
|
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
|
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
|
||||||
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS);
|
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS);
|
||||||
@@ -281,22 +276,28 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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();
|
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
|
||||||
mStreamManager.receive(audioPacket);
|
|
||||||
return;
|
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)
|
if(audioBroadcaster != null)
|
||||||
{
|
{
|
||||||
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
|
audioBroadcaster.setListener(broadcastEvent -> process(broadcastEvent));
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void receive(BroadcastEvent broadcastEvent)
|
|
||||||
{
|
|
||||||
process(broadcastEvent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
audioBroadcaster.start();
|
audioBroadcaster.start();
|
||||||
|
|
||||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
|
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
|
||||||
|
|
||||||
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
|
||||||
|
|
||||||
if(index >= 0)
|
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
|
* 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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * 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);
|
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)
|
if(to != null)
|
||||||
{
|
{
|
||||||
sb.append("TO:").append(to);
|
sb.append("TO:").append(to);
|
||||||
@@ -194,7 +203,17 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
|
|||||||
sb.append("TO:UNKNOWN");
|
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)
|
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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* *
|
||||||
* (at your option) any later version.
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
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);
|
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)
|
if(to != null)
|
||||||
{
|
{
|
||||||
sb.append("TO:").append(to);
|
sb.append("TO:").append(to);
|
||||||
@@ -219,7 +230,17 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
|
|||||||
sb.append("TO:UNKNOWN");
|
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)
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * 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);
|
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)
|
if(to != null)
|
||||||
{
|
{
|
||||||
sbTitle2.append("TO:").append(to);
|
sbTitle2.append("TO:").append(to);
|
||||||
@@ -307,7 +317,17 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
|
|||||||
sbTitle2.append("TO:UNKNOWN");
|
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)
|
if(from != null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.audio.codec.mbe;
|
package io.github.dsheirer.audio.codec.mbe;
|
||||||
|
|
||||||
|
import io.github.dsheirer.alias.AliasList;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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 final String AMBE_CODEC = "AMBE 3600 x 2450";
|
||||||
private static boolean sLibraryStatusLogged = false;
|
private static boolean sLibraryStatusLogged = false;
|
||||||
|
|
||||||
public AmbeAudioModule(UserPreferences userPreferences)
|
public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
|
||||||
{
|
{
|
||||||
super(userPreferences);
|
super(userPreferences, aliasList);
|
||||||
|
|
||||||
if(!sLibraryStatusLogged)
|
if(!sLibraryStatusLogged)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.audio.codec.mbe;
|
package io.github.dsheirer.audio.codec.mbe;
|
||||||
|
|
||||||
|
import io.github.dsheirer.alias.AliasList;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -32,9 +33,9 @@ public abstract class ImbeAudioModule extends JmbeAudioModule
|
|||||||
private static final String IMBE_CODEC = "IMBE";
|
private static final String IMBE_CODEC = "IMBE";
|
||||||
private static boolean sLibraryStatusLogged = false;
|
private static boolean sLibraryStatusLogged = false;
|
||||||
|
|
||||||
public ImbeAudioModule(UserPreferences userPreferences)
|
public ImbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
|
||||||
{
|
{
|
||||||
super(userPreferences);
|
super(userPreferences, aliasList);
|
||||||
|
|
||||||
if(!sLibraryStatusLogged)
|
if(!sLibraryStatusLogged)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,14 +23,15 @@
|
|||||||
package io.github.dsheirer.audio.codec.mbe;
|
package io.github.dsheirer.audio.codec.mbe;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import io.github.dsheirer.alias.AliasList;
|
||||||
import io.github.dsheirer.audio.AbstractAudioModule;
|
import io.github.dsheirer.audio.AbstractAudioModule;
|
||||||
|
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
|
||||||
import io.github.dsheirer.eventbus.MyEventBus;
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
import io.github.dsheirer.message.IMessage;
|
import io.github.dsheirer.message.IMessage;
|
||||||
import io.github.dsheirer.message.IMessageListener;
|
import io.github.dsheirer.message.IMessageListener;
|
||||||
import io.github.dsheirer.preference.PreferenceType;
|
import io.github.dsheirer.preference.PreferenceType;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
|
||||||
import jmbe.iface.IAudioCodec;
|
import jmbe.iface.IAudioCodec;
|
||||||
import jmbe.iface.IAudioCodecLibrary;
|
import jmbe.iface.IAudioCodecLibrary;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -44,17 +45,18 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
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 Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
|
||||||
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
|
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
|
||||||
private static List<String> mLibraryLoadStatusLogged = new ArrayList<>();
|
private static List<String> mLibraryLoadStatusLogged = new ArrayList<>();
|
||||||
private IAudioCodec mAudioCodec;
|
private IAudioCodec mAudioCodec;
|
||||||
private UserPreferences mUserPreferences;
|
private UserPreferences mUserPreferences;
|
||||||
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("JmbeAudioModule");
|
|
||||||
|
|
||||||
public JmbeAudioModule(UserPreferences userPreferences)
|
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
|
||||||
{
|
{
|
||||||
|
super(aliasList);
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
MyEventBus.getEventBus().register(this);
|
MyEventBus.getEventBus().register(this);
|
||||||
loadConverter();
|
loadConverter();
|
||||||
@@ -73,11 +75,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
|||||||
return getAudioCodec() != null;
|
return getAudioCodec() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ReusableAudioPacketQueue getAudioPacketQueue()
|
|
||||||
{
|
|
||||||
return mAudioPacketQueue;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Listener<IMessage> getMessageListener()
|
public Listener<IMessage> getMessageListener()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,21 +22,9 @@
|
|||||||
|
|
||||||
package io.github.dsheirer.audio.codec.mbe;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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
|
* 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);
|
private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceConverter.class);
|
||||||
|
|
||||||
public static void convert(Path input, Path output) throws IOException
|
// public static void convert(Path input, Path output) throws IOException
|
||||||
{
|
// {
|
||||||
InputStream inputStream = Files.newInputStream(input);
|
// InputStream inputStream = Files.newInputStream(input);
|
||||||
ObjectMapper mapper = new ObjectMapper();
|
// ObjectMapper mapper = new ObjectMapper();
|
||||||
MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
|
// MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
|
||||||
convert(sequence, output);
|
// convert(sequence, output);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public static void convert(MBECallSequence callSequence, Path outputPath)
|
// public static void convert(MBECallSequence callSequence, Path outputPath)
|
||||||
{
|
// {
|
||||||
if(callSequence == null || callSequence.isEncrypted())
|
// if(callSequence == null || callSequence.isEncrypted())
|
||||||
{
|
// {
|
||||||
throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
|
// throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if(callSequence != null && !callSequence.isEncrypted())
|
// if(callSequence != null && !callSequence.isEncrypted())
|
||||||
{
|
// {
|
||||||
ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
|
// ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
|
||||||
|
//
|
||||||
AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
|
// AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
|
||||||
recorder.start();
|
// recorder.start();
|
||||||
|
//
|
||||||
long delayMillis = 0;
|
// long delayMillis = 0;
|
||||||
|
//
|
||||||
try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
|
// try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
|
||||||
{
|
// {
|
||||||
thumbDv.start();
|
// thumbDv.start();
|
||||||
for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
|
// for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
|
||||||
{
|
// {
|
||||||
mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
|
// mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
|
||||||
thumbDv.decode(voiceFrame.getFrameBytes());
|
// thumbDv.decode(voiceFrame.getFrameBytes());
|
||||||
delayMillis += 30;
|
// delayMillis += 30;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if(delayMillis > 0)
|
// if(delayMillis > 0)
|
||||||
{
|
// {
|
||||||
delayMillis += 1000;
|
// delayMillis += 1000;
|
||||||
try
|
// try
|
||||||
{
|
// {
|
||||||
Thread.sleep(delayMillis);
|
// Thread.sleep(delayMillis);
|
||||||
}
|
// }
|
||||||
catch(InterruptedException ie)
|
// catch(InterruptedException ie)
|
||||||
{
|
// {
|
||||||
|
//
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
catch(IOException ioe)
|
// catch(IOException ioe)
|
||||||
{
|
// {
|
||||||
mLog.error("Error", ioe);
|
// mLog.error("Error", ioe);
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
|
// recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
public static void main(String[] args)
|
// 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_3_TS0_65084_6591007.mbe";
|
||||||
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe";
|
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe";
|
||||||
//
|
//
|
||||||
@@ -118,5 +106,5 @@ public class MBECallSequenceConverter
|
|||||||
// {
|
// {
|
||||||
// mLog.error("Error", ioe);
|
// mLog.error("Error", ioe);
|
||||||
// }
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.audio.convert;
|
package io.github.dsheirer.audio.convert;
|
||||||
|
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface IAudioConverter
|
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.
|
* 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
|
* 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.AudioFormats;
|
||||||
import io.github.dsheirer.audio.AudioUtils;
|
import io.github.dsheirer.audio.AudioUtils;
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
|
||||||
import net.sourceforge.lame.lowlevel.LameEncoder;
|
import net.sourceforge.lame.lowlevel.LameEncoder;
|
||||||
import net.sourceforge.lame.mp3.Lame;
|
import net.sourceforge.lame.mp3.Lame;
|
||||||
import net.sourceforge.lame.mp3.MPEGMode;
|
import net.sourceforge.lame.mp3.MPEGMode;
|
||||||
@@ -54,7 +53,7 @@ public class MP3AudioConverter implements IAudioConverter
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] convert(List<ReusableAudioPacket> audioPackets)
|
public byte[] convert(List<float[]> audioPackets)
|
||||||
{
|
{
|
||||||
mMP3Stream.reset();
|
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
|
@Override
|
||||||
public byte[] flush()
|
public byte[] flush()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,9 +18,6 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package io.github.dsheirer.audio.convert;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -31,10 +28,11 @@ import java.util.List;
|
|||||||
public class MP3SilenceGenerator implements ISilenceGenerator
|
public class MP3SilenceGenerator implements ISilenceGenerator
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
|
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 byte[] mPreviousPartialFrameData;
|
||||||
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("MP3 Silence Generator");
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates MP3 audio silence frames
|
* Generates MP3 audio silence frames
|
||||||
@@ -46,13 +44,10 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
|||||||
public byte[] generate(long duration)
|
public byte[] generate(long duration)
|
||||||
{
|
{
|
||||||
int length = (int)(duration * 8); //8000 Hz sample rate
|
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<>();
|
List<float[]> silenceBuffers = new ArrayList<>();
|
||||||
silencePackets.add(silencePacket);
|
silenceBuffers.add(new float[length]);
|
||||||
|
byte[] frameData = mGenerator.convert(silenceBuffers);
|
||||||
byte[] frameData = mGenerator.convert(silencePackets);
|
|
||||||
|
|
||||||
frameData = merge(mPreviousPartialFrameData, frameData);
|
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.ReadyResponse;
|
||||||
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse;
|
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
|
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
import io.github.dsheirer.util.ThreadPool;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -82,14 +82,14 @@ public class ThumbDv implements AutoCloseable
|
|||||||
private ScheduledFuture mAudioDecodeProcessorHandle;
|
private ScheduledFuture mAudioDecodeProcessorHandle;
|
||||||
private LinkedTransferQueue<DecodeSpeechRequest> mDecodeSpeechRequests = new LinkedTransferQueue<>();
|
private LinkedTransferQueue<DecodeSpeechRequest> mDecodeSpeechRequests = new LinkedTransferQueue<>();
|
||||||
private AudioProtocol mAudioProtocol;
|
private AudioProtocol mAudioProtocol;
|
||||||
private Listener<ReusableAudioPacket> mAudioPacketListener;
|
private Listener<ReusableFloatBuffer> mAudioBufferListener;
|
||||||
private ReusableAudioPacketQueue mReusableAudioPacketQueue = new ReusableAudioPacketQueue("ThumbDv");
|
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("ThumbDv");
|
||||||
private boolean mStarted;
|
private boolean mStarted;
|
||||||
|
|
||||||
public ThumbDv(AudioProtocol audioProtocol, Listener<ReusableAudioPacket> listener)
|
public ThumbDv(AudioProtocol audioProtocol, Listener<ReusableFloatBuffer> listener)
|
||||||
{
|
{
|
||||||
mAudioProtocol = audioProtocol;
|
mAudioProtocol = audioProtocol;
|
||||||
mAudioPacketListener = listener;
|
mAudioBufferListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -206,12 +206,11 @@ public class ThumbDv implements AutoCloseable
|
|||||||
{
|
{
|
||||||
AmbeMessage message = AmbeMessageFactory.getMessage(bytes);
|
AmbeMessage message = AmbeMessageFactory.getMessage(bytes);
|
||||||
|
|
||||||
if(message instanceof DecodeSpeechResponse && mAudioPacketListener != null)
|
if(message instanceof DecodeSpeechResponse && mAudioBufferListener != null)
|
||||||
{
|
{
|
||||||
float[] samples = ((DecodeSpeechResponse)message).getSamples();
|
float[] samples = ((DecodeSpeechResponse)message).getSamples();
|
||||||
ReusableAudioPacket audioPacket = mReusableAudioPacketQueue.getBuffer(samples.length);
|
ReusableFloatBuffer audioBuffer = mReusableBufferQueue.getBuffer(samples, System.currentTimeMillis());
|
||||||
audioPacket.loadAudioFrom(samples);
|
mAudioBufferListener.receive(audioBuffer);
|
||||||
mAudioPacketListener.receive(audioPacket);
|
|
||||||
}
|
}
|
||||||
else if(message instanceof ReadyResponse && mAudioDecodeProcessorHandle == null)
|
else if(message instanceof ReadyResponse && mAudioDecodeProcessorHandle == null)
|
||||||
{
|
{
|
||||||
@@ -457,7 +456,7 @@ public class ThumbDv implements AutoCloseable
|
|||||||
|
|
||||||
mLog.debug("Starting thumb dv thread(s)");
|
mLog.debug("Starting thumb dv thread(s)");
|
||||||
|
|
||||||
final Listener<ReusableAudioPacket> listener = reusableAudioPacket -> {
|
final Listener<ReusableFloatBuffer> listener = reusableAudioPacket -> {
|
||||||
mLog.info("Got an audio packet!");
|
mLog.info("Got an audio packet!");
|
||||||
reusableAudioPacket.decrementUserCount();
|
reusableAudioPacket.decrementUserCount();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ import java.awt.EventQueue;
|
|||||||
import java.awt.Font;
|
import java.awt.Font;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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
|
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 Identifier mIdentifier;
|
||||||
private List<Alias> mAliases = Collections.EMPTY_LIST;
|
private List<Alias> mAliases = Collections.EMPTY_LIST;
|
||||||
private AliasModel mAliasModel;
|
private AliasModel mAliasModel;
|
||||||
|
private Lock mLock = new ReentrantLock();
|
||||||
|
|
||||||
public AudioChannelPanel(IconManager iconManager, UserPreferences userPreferences, SettingsManager settingsManager,
|
public AudioChannelPanel(IconManager iconManager, UserPreferences userPreferences, SettingsManager settingsManager,
|
||||||
AudioOutput audioOutput, AliasModel aliasModel)
|
AudioOutput audioOutput, AliasModel aliasModel)
|
||||||
@@ -127,6 +130,12 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
{
|
{
|
||||||
//Deregister from receiving preference update notifications
|
//Deregister from receiving preference update notifications
|
||||||
MyEventBus.getEventBus().unregister(this);
|
MyEventBus.getEventBus().unregister(this);
|
||||||
|
|
||||||
|
if(mAudioOutput != null)
|
||||||
|
{
|
||||||
|
mAudioOutput.removeAudioEventListener(this);
|
||||||
|
mAudioOutput.removeAudioMetadataListener();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void init()
|
private void init()
|
||||||
@@ -193,13 +202,24 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
*/
|
*/
|
||||||
private void resetLabels()
|
private void resetLabels()
|
||||||
{
|
{
|
||||||
boolean updated = mIdentifier != null;
|
//Protect access to mIdentifier and mAliases
|
||||||
mIdentifier = null;
|
mLock.lock();
|
||||||
mAliases = Collections.EMPTY_LIST;
|
|
||||||
|
|
||||||
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;
|
boolean updated = false;
|
||||||
|
|
||||||
if(toIds.size() == 1)
|
//Protect access to mIdentifier and mAliases
|
||||||
{
|
mLock.lock();
|
||||||
Identifier currentIdentifier = mIdentifier;
|
|
||||||
|
|
||||||
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);
|
mIdentifier = toIds.get(0);
|
||||||
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
|
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
|
||||||
@@ -236,69 +273,64 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
}
|
}
|
||||||
updated = true;
|
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;
|
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
if(updated)
|
|
||||||
{
|
{
|
||||||
updateLabels();
|
mLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the alias label with text and icon from the alias. Note: this
|
* Updates the alias label with text and icon from the alias.
|
||||||
* does not occur on the Swing event thread -- wrap any calls to this
|
|
||||||
* method with an event thread call.
|
|
||||||
*/
|
*/
|
||||||
private void updateLabels()
|
private void updateLabels()
|
||||||
{
|
{
|
||||||
String identifier = null;
|
String identifier = null;
|
||||||
String iconName = null;
|
String iconName = null;
|
||||||
|
|
||||||
if(mAliases.size() == 1)
|
//Protect access to mIdentifier and mAliases
|
||||||
{
|
mLock.lock();
|
||||||
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)
|
try
|
||||||
{
|
{
|
||||||
identifier = mTalkgroupFormatPreference.format(mIdentifier);
|
if(mAliases.size() == 1)
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
|
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);
|
mIdentifierLabel.setText(identifierText);
|
||||||
mIconLabel.setIcon(icon);
|
mIconLabel.setIcon(icon);
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,14 +355,16 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
|
|||||||
switch(colorSetting.getColorSettingName())
|
switch(colorSetting.getColorSettingName())
|
||||||
{
|
{
|
||||||
case CHANNEL_STATE_LABEL_DECODER:
|
case CHANNEL_STATE_LABEL_DECODER:
|
||||||
if(mIdentifierLabel != null)
|
EventQueue.invokeLater(() -> {
|
||||||
{
|
if(mIdentifierLabel != null)
|
||||||
mIdentifierLabel.setForeground(mLabelColor);
|
{
|
||||||
}
|
mIdentifierLabel.setForeground(mLabelColor);
|
||||||
if(mIconLabel != null)
|
}
|
||||||
{
|
if(mIconLabel != null)
|
||||||
mIconLabel.setForeground(mLabelColor);
|
{
|
||||||
}
|
mIconLabel.setForeground(mLabelColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,13 +21,23 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.audio.playback;
|
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.AudioEvent;
|
||||||
|
import io.github.dsheirer.audio.AudioSegment;
|
||||||
|
import io.github.dsheirer.eventbus.MyEventBus;
|
||||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
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.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
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.MixerChannel;
|
||||||
import io.github.dsheirer.util.ThreadPool;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -42,42 +52,41 @@ import javax.sound.sampled.LineUnavailableException;
|
|||||||
import javax.sound.sampled.Mixer;
|
import javax.sound.sampled.Mixer;
|
||||||
import javax.sound.sampled.SourceDataLine;
|
import javax.sound.sampled.SourceDataLine;
|
||||||
import java.nio.ByteBuffer;
|
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.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
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 final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||||
|
|
||||||
private LinkedTransferQueue<ReusableAudioPacket> mBuffer = new LinkedTransferQueue<>();
|
|
||||||
private int mBufferStartThreshold;
|
private int mBufferStartThreshold;
|
||||||
private int mBufferStopThreshold;
|
private int mBufferStopThreshold;
|
||||||
private static IdentifierCollection EMPTY_IDENTIFIER_COLLECTION = new IdentifierCollection(0);
|
|
||||||
static
|
|
||||||
{
|
|
||||||
EMPTY_IDENTIFIER_COLLECTION.setUpdated(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||||
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
|
||||||
|
private ScheduledFuture<?> mProcessorFuture;
|
||||||
private ScheduledFuture<?> mProcessorTask;
|
|
||||||
|
|
||||||
private SourceDataLine mOutput;
|
private SourceDataLine mOutput;
|
||||||
private Mixer mMixer;
|
private Mixer mMixer;
|
||||||
private MixerChannel mMixerChannel;
|
private MixerChannel mMixerChannel;
|
||||||
private FloatControl mGainControl;
|
private FloatControl mGainControl;
|
||||||
private BooleanControl mMuteControl;
|
private BooleanControl mMuteControl;
|
||||||
|
|
||||||
private AudioEvent mAudioStartEvent;
|
private AudioEvent mAudioStartEvent;
|
||||||
private AudioEvent mAudioStopEvent;
|
private AudioEvent mAudioStopEvent;
|
||||||
|
|
||||||
private boolean mCanProcessAudio = false;
|
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
|
* 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
|
* @param requestedBufferSize of approximately 1 second of audio
|
||||||
*/
|
*/
|
||||||
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
|
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
|
||||||
int requestedBufferSize)
|
int requestedBufferSize, UserPreferences userPreferences)
|
||||||
{
|
{
|
||||||
mMixer = mixer;
|
mMixer = mixer;
|
||||||
mMixerChannel = mixerChannel;
|
mMixerChannel = mixerChannel;
|
||||||
|
mUserPreferences = userPreferences;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -139,46 +149,395 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
|||||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Run the queue processor task every 40 milliseconds or 25 times a second */
|
//Run the queue processor task every 100 milliseconds or 10 times a second
|
||||||
mProcessorTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(),
|
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||||
0, 40, TimeUnit.MILLISECONDS);
|
0, 100, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED,
|
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||||
getChannelName());
|
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
||||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED,
|
|
||||||
getChannelName());
|
|
||||||
|
|
||||||
mCanProcessAudio = true;
|
mCanProcessAudio = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch(LineUnavailableException e)
|
catch(LineUnavailableException e)
|
||||||
{
|
{
|
||||||
mLog.error("Couldn't obtain audio source data line for "
|
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
|
||||||
+ "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]");
|
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()
|
public void dispose()
|
||||||
{
|
{
|
||||||
mCanProcessAudio = false;
|
mCanProcessAudio = false;
|
||||||
|
|
||||||
if(mProcessorTask != null)
|
if(mProcessorFuture != null)
|
||||||
{
|
{
|
||||||
mProcessorTask.cancel(true);
|
mProcessorFuture.cancel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
mProcessorTask = null;
|
mProcessorFuture = null;
|
||||||
|
|
||||||
mBuffer.clear();
|
mLock.lock();
|
||||||
|
|
||||||
mAudioEventBroadcaster.dispose();
|
try
|
||||||
mAudioEventBroadcaster = null;
|
{
|
||||||
|
if(mNextAudioSegment != null)
|
||||||
|
{
|
||||||
|
mNextAudioSegment.decrementConsumerCount();
|
||||||
|
mNextAudioSegment = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mLock.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(mCurrentAudioSegment);
|
||||||
|
mCurrentAudioSegment = null;
|
||||||
|
|
||||||
|
mAudioEventBroadcaster.clear();
|
||||||
mIdentifierCollectionListener = null;
|
mIdentifierCollectionListener = null;
|
||||||
|
|
||||||
if(mOutput != 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
|
* Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line.
|
||||||
* the underlying source data line.
|
|
||||||
*/
|
*/
|
||||||
protected abstract ByteBuffer convert(ReusableAudioPacket packet);
|
protected abstract ByteBuffer convert(float[] buffer);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audio output channel name
|
* Audio output channel name
|
||||||
@@ -229,7 +587,7 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
|||||||
/**
|
/**
|
||||||
* Broadcasts an audio event to the registered listener
|
* Broadcasts an audio event to the registered listener
|
||||||
*/
|
*/
|
||||||
private void broadcast(AudioEvent audioEvent)
|
private void broadcastAudioEvent(AudioEvent audioEvent)
|
||||||
{
|
{
|
||||||
mAudioEventBroadcaster.broadcast(audioEvent);
|
mAudioEventBroadcaster.broadcast(audioEvent);
|
||||||
}
|
}
|
||||||
@@ -243,6 +601,9 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
|||||||
mIdentifierCollectionListener = listener;
|
mIdentifierCollectionListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregisters the current audio metadata listener
|
||||||
|
*/
|
||||||
public void removeAudioMetadataListener()
|
public void removeAudioMetadataListener()
|
||||||
{
|
{
|
||||||
mIdentifierCollectionListener = null;
|
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();
|
if(mRunning)
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void receive(ReusableAudioPacket packet)
|
|
||||||
{
|
|
||||||
if(mCanProcessAudio)
|
|
||||||
{
|
{
|
||||||
//Update the activity timestamp so that this audio output doesn't
|
//If the output buffer falls below the threshold then drain the output and stop playback
|
||||||
//get disconnected before it starts processing the audio stream
|
if(mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
mOutput.drain();
|
mOutput.drain();
|
||||||
mOutput.stop();
|
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)
|
if(mMuteControl != null)
|
||||||
{
|
{
|
||||||
mMuteControl.setValue(muted);
|
mMuteControl.setValue(muted);
|
||||||
|
broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
|
||||||
broadcast(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;
|
return mGainControl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if this audio output has a gain control available
|
||||||
|
*/
|
||||||
public boolean hasGainControl()
|
public boolean hasGainControl()
|
||||||
{
|
{
|
||||||
return mGainControl != null;
|
return mGainControl != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitors the source data line playback state and broadcasts audio events
|
* Monitors the source data line playback state and broadcasts audio events to the registered listener as the
|
||||||
* to the registered listener as the state changes
|
* state changes
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void update(LineEvent event)
|
public void update(LineEvent event)
|
||||||
@@ -454,4 +724,23 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
|
|||||||
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
|
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.AudioEvent;
|
||||||
import io.github.dsheirer.audio.AudioException;
|
import io.github.dsheirer.audio.AudioException;
|
||||||
import io.github.dsheirer.audio.IAudioController;
|
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.icon.IconManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.settings.SettingsManager;
|
import io.github.dsheirer.settings.SettingsManager;
|
||||||
import io.github.dsheirer.source.SourceManager;
|
import io.github.dsheirer.source.SourceManager;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
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 net.miginfocom.swing.MigLayout;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.sound.sampled.FloatControl;
|
import javax.sound.sampled.FloatControl;
|
||||||
|
import javax.swing.Icon;
|
||||||
import javax.swing.ImageIcon;
|
import javax.swing.ImageIcon;
|
||||||
import javax.swing.JButton;
|
import javax.swing.JButton;
|
||||||
import javax.swing.JMenu;
|
import javax.swing.JMenu;
|
||||||
@@ -51,6 +58,7 @@ import java.awt.event.ActionEvent;
|
|||||||
import java.awt.event.ActionListener;
|
import java.awt.event.ActionListener;
|
||||||
import java.awt.event.MouseEvent;
|
import java.awt.event.MouseEvent;
|
||||||
import java.awt.event.MouseListener;
|
import java.awt.event.MouseListener;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
||||||
{
|
{
|
||||||
@@ -169,34 +177,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
|
|||||||
JPopupMenu popup = new JPopupMenu();
|
JPopupMenu popup = new JPopupMenu();
|
||||||
|
|
||||||
/* Audio mixer/output selection menus */
|
/* Audio mixer/output selection menus */
|
||||||
JMenu outputMenu = new JMenu("Audio Output");
|
JMenuItem outputMenu = new JMenuItem("Configure ...");
|
||||||
|
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
|
||||||
MixerChannelConfiguration[] mixerConfigurations =
|
outputMenu.setIcon(icon);
|
||||||
mSourceManager.getMixerManager().getOutputMixers();
|
outputMenu.addActionListener(new ActionListener()
|
||||||
|
|
||||||
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
|
|
||||||
{
|
{
|
||||||
MixerSelectionItem mixerItem = new MixerSelectionItem(mixerConfig);
|
@Override
|
||||||
|
public void actionPerformed(ActionEvent e)
|
||||||
try
|
|
||||||
{
|
{
|
||||||
MixerChannelConfiguration current = mController.getMixerChannelConfiguration();
|
MyEventBus.getEventBus().post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK));
|
||||||
|
|
||||||
if(current != null && current.equals(mixerConfig))
|
|
||||||
{
|
|
||||||
mixerItem.setSelected(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(AudioException e)
|
});
|
||||||
{
|
|
||||||
mLog.error("Error while detecting current mixer "
|
|
||||||
+ "channel configuration", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
outputMenu.add(mixerItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.add(outputMenu);
|
popup.add(outputMenu);
|
||||||
|
popup.add(new JPopupMenu.Separator());
|
||||||
|
|
||||||
/* Audio output mute and volume control */
|
/* Audio output mute and volume control */
|
||||||
for(AudioOutput output : mController.getAudioOutputs())
|
for(AudioOutput output : mController.getAudioOutputs())
|
||||||
|
|||||||
@@ -19,13 +19,17 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.audio.playback;
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
import io.github.dsheirer.audio.AudioEvent;
|
import io.github.dsheirer.audio.AudioEvent;
|
||||||
import io.github.dsheirer.audio.AudioException;
|
import io.github.dsheirer.audio.AudioException;
|
||||||
|
import io.github.dsheirer.audio.AudioSegment;
|
||||||
import io.github.dsheirer.audio.IAudioController;
|
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.properties.SystemProperties;
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
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.MixerChannel;
|
||||||
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
|
||||||
import io.github.dsheirer.source.mixer.MixerManager;
|
import io.github.dsheirer.source.mixer.MixerManager;
|
||||||
@@ -38,113 +42,206 @@ import javax.sound.sampled.Mixer;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.LinkedTransferQueue;
|
import java.util.concurrent.LinkedTransferQueue;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
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);
|
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_CHANNELS_PROPERTY = "audio.manager.channels";
|
||||||
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
|
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
|
||||||
|
|
||||||
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
|
||||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
|
||||||
|
|
||||||
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
|
||||||
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
|
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 Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
|
||||||
|
|
||||||
private ScheduledFuture<?> mProcessingTask;
|
private ScheduledFuture<?> mProcessingTask;
|
||||||
private MixerManager mMixerManager;
|
private UserPreferences mUserPreferences;
|
||||||
private MixerChannelConfiguration mMixerChannelConfiguration;
|
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
|
* Constructs an instance.
|
||||||
* packets to any combination of outputs based on any alias audio routing
|
*
|
||||||
* options specified by the user.
|
* @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();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
if(configuration != null)
|
||||||
* 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(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
|
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 */
|
mNewAudioSegmentQueue.add(audioSegment);
|
||||||
Mixer defaultMixer = AudioSystem.getMixer(null);
|
}
|
||||||
|
|
||||||
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()
|
public void dispose()
|
||||||
@@ -152,68 +249,34 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
|||||||
if(mProcessingTask != null)
|
if(mProcessingTask != null)
|
||||||
{
|
{
|
||||||
mProcessingTask.cancel(true);
|
mProcessingTask.cancel(true);
|
||||||
|
mProcessingTask = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
mAudioPacketQueue.clear();
|
mNewAudioSegmentQueue.clear();
|
||||||
|
mAudioSegments.clear();
|
||||||
mProcessingTask = null;
|
|
||||||
|
|
||||||
mChannelConnectionMap.clear();
|
|
||||||
|
|
||||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
|
||||||
{
|
|
||||||
connection.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
mAudioOutputConnections.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
|
@Subscribe
|
||||||
public synchronized void receive(ReusableAudioPacket packet)
|
public void preferenceUpdated(PreferenceType preferenceType)
|
||||||
{
|
{
|
||||||
mAudioPacketQueue.add(packet);
|
if(preferenceType == PreferenceType.PLAYBACK)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks each audio channel assignment and disconnects any inactive connections
|
|
||||||
*/
|
|
||||||
private void disconnectInactiveChannelAssignments()
|
|
||||||
{
|
|
||||||
boolean changed = false;
|
|
||||||
|
|
||||||
for(AudioOutputConnection connection : mAudioOutputConnections)
|
|
||||||
{
|
{
|
||||||
if(connection.isInactive() && mChannelConnectionMap.containsKey(connection.getAudioChannelId()))
|
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
|
||||||
|
|
||||||
|
if(configuration != null && !configuration.equals(mMixerChannelConfiguration))
|
||||||
{
|
{
|
||||||
mChannelConnectionMap.remove(connection.getAudioChannelId());
|
try
|
||||||
connection.disconnect();
|
{
|
||||||
mAvailableConnectionCount++;
|
setMixerChannelConfiguration(configuration);
|
||||||
changed = true;
|
}
|
||||||
}
|
catch(AudioException ae)
|
||||||
}
|
{
|
||||||
|
mLog.error("Error changing audio output to [" + configuration.toString() + "]", ae);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,19 +290,7 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
|||||||
@Override
|
@Override
|
||||||
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
|
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
|
||||||
{
|
{
|
||||||
setMixerChannelConfiguration(entry, true);
|
if(entry != null)
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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))
|
|
||||||
{
|
{
|
||||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
||||||
|
|
||||||
@@ -248,68 +299,37 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
|
|||||||
mProcessingTask.cancel(true);
|
mProcessingTask.cancel(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
disposeCurrentConfiguration();
|
for(AudioOutput audioOutput: mAudioOutputs)
|
||||||
|
{
|
||||||
|
audioOutput.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
mAudioOutputs.clear();
|
||||||
|
|
||||||
switch(entry.getMixerChannel())
|
switch(entry.getMixerChannel())
|
||||||
{
|
{
|
||||||
case MONO:
|
case MONO:
|
||||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer());
|
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||||
mAudioOutputConnections.add(new AudioOutputConnection(mono));
|
mAudioOutputs.add(mono);
|
||||||
mAvailableConnectionCount++;
|
|
||||||
mAudioOutputMap.put(mono.getChannelName(), mono);
|
|
||||||
break;
|
break;
|
||||||
case STEREO:
|
case STEREO:
|
||||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT);
|
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
||||||
mAudioOutputConnections.add(new AudioOutputConnection(left));
|
mAudioOutputs.add(left);
|
||||||
mAvailableConnectionCount++;
|
|
||||||
mAudioOutputMap.put(left.getChannelName(), left);
|
|
||||||
|
|
||||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT);
|
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
||||||
mAudioOutputConnections.add(new AudioOutputConnection(right));
|
mAudioOutputs.add(right);
|
||||||
mAvailableConnectionCount++;
|
|
||||||
mAudioOutputMap.put(right.getChannelName(), right);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new AudioException("Unsupported mixer channel "
|
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||||
+ "configuration: " + entry.getMixerChannel());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioPacketProcessor(),
|
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||||
0, 15, TimeUnit.MILLISECONDS);
|
0, 100, TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
||||||
|
mMixerChannelConfiguration = entry;
|
||||||
if(saveSettings)
|
|
||||||
{
|
|
||||||
SystemProperties properties = SystemProperties.getInstance();
|
|
||||||
properties.set(AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName());
|
|
||||||
properties.set(AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
* 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
|
@Override
|
||||||
public List<AudioOutput> getAudioOutputs()
|
public List<AudioOutput> getAudioOutputs()
|
||||||
{
|
{
|
||||||
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputMap.values());
|
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputs);
|
||||||
|
|
||||||
Collections.sort(outputs, new Comparator<AudioOutput>()
|
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
|
* Scheduled runnable to process incoming audio segments
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
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
|
@Override
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
disconnectInactiveChannelAssignments();
|
processAudioSegments();
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(Exception e)
|
catch(Throwable t)
|
||||||
{
|
{
|
||||||
mLog.error("Encountered error while processing audio packets", e);
|
mLog.error("Encountered error while processing audio segments", t);
|
||||||
|
|
||||||
while(mPackets.size() > 0)
|
|
||||||
{
|
|
||||||
ReusableAudioPacket audioPacket = mPackets.remove(0);
|
|
||||||
audioPacket.decrementUserCount();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
import io.github.dsheirer.audio.AudioFormats;
|
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 io.github.dsheirer.source.mixer.MixerChannel;
|
||||||
|
|
||||||
import javax.sound.sampled.Mixer;
|
import javax.sound.sampled.Mixer;
|
||||||
@@ -34,23 +34,21 @@ public class MonoAudioOutput extends AudioOutput
|
|||||||
{
|
{
|
||||||
private final static int BUFFER_SIZE = 8000;
|
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,
|
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.
|
* Converts the audio packet data into mono audio frames.
|
||||||
*/
|
*/
|
||||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
protected ByteBuffer convert(float[] samples)
|
||||||
{
|
{
|
||||||
ByteBuffer buffer = null;
|
ByteBuffer buffer = null;
|
||||||
|
|
||||||
if(packet.hasAudioSamples())
|
if(samples.length > 0)
|
||||||
{
|
{
|
||||||
float[] samples = packet.getAudioSamples();
|
|
||||||
|
|
||||||
/* Little-endian byte buffer */
|
/* Little-endian byte buffer */
|
||||||
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
@@ -62,8 +60,6 @@ public class MonoAudioOutput extends AudioOutput
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packet.decrementUserCount();
|
|
||||||
|
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
package io.github.dsheirer.audio.playback;
|
package io.github.dsheirer.audio.playback;
|
||||||
|
|
||||||
import io.github.dsheirer.audio.AudioFormats;
|
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 io.github.dsheirer.source.mixer.MixerChannel;
|
||||||
|
|
||||||
import javax.sound.sampled.Mixer;
|
import javax.sound.sampled.Mixer;
|
||||||
@@ -34,10 +34,10 @@ public class StereoAudioOutput extends AudioOutput
|
|||||||
{
|
{
|
||||||
private final static int BUFFER_SIZE = 16000;
|
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,
|
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
|
* channel containing the audio and the other channel containing zero
|
||||||
* valued (silent) samples.
|
* valued (silent) samples.
|
||||||
*/
|
*/
|
||||||
protected ByteBuffer convert(ReusableAudioPacket packet)
|
protected ByteBuffer convert(float[] samples)
|
||||||
{
|
{
|
||||||
ByteBuffer buffer = null;
|
ByteBuffer buffer = null;
|
||||||
|
|
||||||
if(packet.hasAudioSamples())
|
if(samples.length > 0)
|
||||||
{
|
{
|
||||||
float[] samples = packet.getAudioSamples();
|
|
||||||
|
|
||||||
/* Little-endian byte buffer */
|
/* Little-endian byte buffer */
|
||||||
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
|
||||||
|
|
||||||
@@ -76,8 +74,6 @@ public class StereoAudioOutput extends AudioOutput
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
packet.decrementUserCount();
|
|
||||||
|
|
||||||
return buffer;
|
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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.bits;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
@@ -94,8 +100,8 @@ public class MultiSyncPatternMatcher
|
|||||||
|
|
||||||
if(mBitCount > mSyncLossThreshold)
|
if(mBitCount > mSyncLossThreshold)
|
||||||
{
|
{
|
||||||
|
mSyncDetectListener.syncLost(mBitCount);
|
||||||
mBitCount = 0;
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * it under the terms of the GNU General Public License as published by
|
||||||
@@ -261,7 +261,12 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
|||||||
@Override
|
@Override
|
||||||
public void receive(IdentifierUpdateNotification update)
|
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();
|
Identifier identifier = update.getIdentifier();
|
||||||
|
|
||||||
switch(identifier.getIdentifierClass())
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * 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
|
else
|
||||||
{
|
{
|
||||||
return id.getValue() + " TS:" + channelMetadata.getTimeslot();
|
String value = id.getValue().replace(" TS0", "").replace(" TS1", "");
|
||||||
|
return value + " TS:" + channelMetadata.getTimeslot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,29 +1,33 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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.channel.state;
|
package io.github.dsheirer.channel.state;
|
||||||
|
|
||||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
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.message.IMessage;
|
||||||
import io.github.dsheirer.module.decode.DecoderType;
|
import io.github.dsheirer.module.decode.DecoderType;
|
||||||
|
import io.github.dsheirer.protocol.Protocol;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -35,6 +39,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
|||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
|
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
|
||||||
private static final String NO_SQUELCH = "No Squelch";
|
private static final String NO_SQUELCH = "No Squelch";
|
||||||
|
private Identifier mChannelNameIdentifier;
|
||||||
|
|
||||||
private DecoderType mDecoderType;
|
private DecoderType mDecoderType;
|
||||||
private String mChannelName;
|
private String mChannelName;
|
||||||
@@ -42,7 +47,8 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
|||||||
public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName)
|
public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName)
|
||||||
{
|
{
|
||||||
mDecoderType = decoderType;
|
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
|
@Override
|
||||||
@@ -74,9 +80,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
|||||||
{
|
{
|
||||||
if(event.getEvent() == Event.RESET)
|
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
|
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||||
//a primary ID for any audio produced by this state.
|
|
||||||
// broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +93,8 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
|
|||||||
@Override
|
@Override
|
||||||
public void start()
|
public void start()
|
||||||
{
|
{
|
||||||
// broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
|
|
||||||
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
|
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
|
||||||
|
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
//Set the proxy as a listener so that echo'd updates are broadcast externally
|
||||||
mutableIdentifierCollection.setIdentifierUpdateListener(mIdentifierUpdateNotificationProxy);
|
mutableIdentifierCollection.setIdentifierUpdateListener(mIdentifierUpdateNotificationProxy);
|
||||||
|
|
||||||
StateMachine stateMachine = new StateMachine(timeslot);
|
StateMachine stateMachine = new StateMachine(timeslot, State.MULTI_CHANNEL_ACTIVE_STATES);
|
||||||
mStateMachineMap.put(timeslot, stateMachine);
|
mStateMachineMap.put(timeslot, stateMachine);
|
||||||
stateMachine.addListener(this);
|
stateMachine.addListener(this);
|
||||||
|
|
||||||
@@ -133,7 +133,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
public void stateChanged(State state, int timeslot)
|
public void stateChanged(State state, int timeslot)
|
||||||
{
|
{
|
||||||
//Broadcast current channel state so that channel rotation monitor can track
|
//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));
|
broadcast(DecoderStateEvent.activeState(timeslot));
|
||||||
}
|
}
|
||||||
@@ -148,9 +148,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
|
|
||||||
switch(state)
|
switch(state)
|
||||||
{
|
{
|
||||||
case IDLE:
|
|
||||||
broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE));
|
|
||||||
break;
|
|
||||||
case RESET:
|
case RESET:
|
||||||
reset(timeslot);
|
reset(timeslot);
|
||||||
mStateMachineMap.get(timeslot).setState(State.IDLE);
|
mStateMachineMap.get(timeslot).setState(State.IDLE);
|
||||||
@@ -173,19 +170,49 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
*/
|
*/
|
||||||
private void checkTeardown()
|
private void checkTeardown()
|
||||||
{
|
{
|
||||||
boolean teardown = true;
|
boolean teardown = false;
|
||||||
|
boolean active = false;
|
||||||
|
|
||||||
for(StateMachine stateMachine: mStateMachineMap.values())
|
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++)
|
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
|
||||||
{
|
{
|
||||||
mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers();
|
mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers();
|
||||||
|
mStateMachineMap.get(timeslot).setState(State.RESET);
|
||||||
if(mChannel.getChannelType() == ChannelType.TRAFFIC)
|
|
||||||
{
|
|
||||||
mStateMachineMap.get(timeslot).setState(State.ACTIVE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +499,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
//in an active (ie sync locked) state.
|
//in an active (ie sync locked) state.
|
||||||
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
|
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(),
|
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
|
||||||
mChannel.getChannelType().name()));
|
mChannel.getChannelType().name()));
|
||||||
@@ -511,7 +534,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
case CONTINUATION:
|
case CONTINUATION:
|
||||||
case DECODE:
|
case DECODE:
|
||||||
case START:
|
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());
|
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
|
||||||
}
|
}
|
||||||
@@ -519,11 +542,11 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
|||||||
case END:
|
case END:
|
||||||
if(mChannel.isTrafficChannel())
|
if(mChannel.isTrafficChannel())
|
||||||
{
|
{
|
||||||
mStateMachineMap.get(event.getTimeslot()).setState(State.TEARDOWN);
|
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mStateMachineMap.get(event.getTimeslot()).setState(State.FADE);
|
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case RESET:
|
case RESET:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
|||||||
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
|
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
|
||||||
private SourceEventListener mInternalSourceEventListener;
|
private SourceEventListener mInternalSourceEventListener;
|
||||||
private ChannelMetadata mChannelMetadata;
|
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);
|
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
|
||||||
|
|
||||||
public SingleChannelState(Channel channel, AliasModel aliasModel)
|
public SingleChannelState(Channel channel, AliasModel aliasModel)
|
||||||
@@ -123,7 +123,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
|||||||
public void stateChanged(State state, int timeslot)
|
public void stateChanged(State state, int timeslot)
|
||||||
{
|
{
|
||||||
//Broadcast current channel state so that channel rotation monitor can track
|
//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));
|
broadcast(DecoderStateEvent.activeState(timeslot));
|
||||||
}
|
}
|
||||||
@@ -377,7 +377,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
|||||||
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
|
||||||
//Rebroadcast frequency error measurements to external listener if we're currently
|
//Rebroadcast frequency error measurements to external listener if we're currently
|
||||||
//in an active (ie sync locked) state.
|
//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(),
|
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
|
||||||
mChannel.getChannelType().name()));
|
mChannel.getChannelType().name()));
|
||||||
@@ -411,7 +411,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
|||||||
case CONTINUATION:
|
case CONTINUATION:
|
||||||
case DECODE:
|
case DECODE:
|
||||||
case START:
|
case START:
|
||||||
if(State.CALL_STATES.contains(event.getState()))
|
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(event.getState()))
|
||||||
{
|
{
|
||||||
mStateMachine.setState(event.getState());
|
mStateMachine.setState(event.getState());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ public enum State
|
|||||||
state == DATA ||
|
state == DATA ||
|
||||||
state == ENCRYPTED ||
|
state == ENCRYPTED ||
|
||||||
state == FADE ||
|
state == FADE ||
|
||||||
|
state == IDLE ||
|
||||||
state == TEARDOWN ||
|
state == TEARDOWN ||
|
||||||
state == RESET;
|
state == RESET;
|
||||||
}
|
}
|
||||||
@@ -60,6 +61,7 @@ public enum State
|
|||||||
state == DATA ||
|
state == DATA ||
|
||||||
state == ENCRYPTED ||
|
state == ENCRYPTED ||
|
||||||
state == FADE ||
|
state == FADE ||
|
||||||
|
state == IDLE ||
|
||||||
state == TEARDOWN ||
|
state == TEARDOWN ||
|
||||||
state == RESET;
|
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")
|
IDLE("IDLE")
|
||||||
{
|
{
|
||||||
@@ -159,8 +162,8 @@ public enum State
|
|||||||
|
|
||||||
private String mDisplayValue;
|
private String mDisplayValue;
|
||||||
|
|
||||||
public static final EnumSet<State> CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
|
public static final EnumSet<State> SINGLE_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
|
||||||
public static final EnumSet<State> IDLE_STATES = EnumSet.of(IDLE, FADE);
|
public static final EnumSet<State> MULTI_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED, IDLE);
|
||||||
|
|
||||||
private State(String displayValue)
|
private State(String displayValue)
|
||||||
{
|
{
|
||||||
@@ -169,17 +172,6 @@ public enum State
|
|||||||
|
|
||||||
public abstract boolean canChangeTo(State 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()
|
public String getDisplayValue()
|
||||||
{
|
{
|
||||||
return mDisplayValue;
|
return mDisplayValue;
|
||||||
|
|||||||
@@ -30,8 +30,12 @@ import org.slf4j.Logger;
|
|||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State machine for tracking a channel state.
|
||||||
|
*/
|
||||||
public class StateMachine
|
public class StateMachine
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(StateMachine.class);
|
private final static Logger mLog = LoggerFactory.getLogger(StateMachine.class);
|
||||||
@@ -42,13 +46,21 @@ public class StateMachine
|
|||||||
protected long mEndTimeout;
|
protected long mEndTimeout;
|
||||||
protected long mEndTimeoutBuffer = 0;
|
protected long mEndTimeoutBuffer = 0;
|
||||||
private int mTimeslot;
|
private int mTimeslot;
|
||||||
|
private EnumSet<State> mActiveStates;
|
||||||
private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD;
|
private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD;
|
||||||
private List<IStateMachineListener> mStateMachineListeners = new ArrayList<>();
|
private List<IStateMachineListener> mStateMachineListeners = new ArrayList<>();
|
||||||
private Listener<IdentifierUpdateNotification> mIdentifierUpdateListener;
|
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;
|
mTimeslot = timeslot;
|
||||||
|
mActiveStates = activeStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addListener(IStateMachineListener listener)
|
public void addListener(IStateMachineListener listener)
|
||||||
@@ -73,7 +85,7 @@ public class StateMachine
|
|||||||
|
|
||||||
public void checkState()
|
public void checkState()
|
||||||
{
|
{
|
||||||
if(mState.isActiveState() && mFadeTimeout <= System.currentTimeMillis())
|
if(mActiveStates.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
|
||||||
{
|
{
|
||||||
setState(State.FADE);
|
setState(State.FADE);
|
||||||
}
|
}
|
||||||
@@ -92,18 +104,22 @@ public class StateMachine
|
|||||||
{
|
{
|
||||||
if(state == mState)
|
if(state == mState)
|
||||||
{
|
{
|
||||||
if(State.CALL_STATES.contains(state))
|
if(mActiveStates.contains(state))
|
||||||
{
|
{
|
||||||
updateFadeTimeout();
|
updateFadeTimeout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(mState.canChangeTo(state))
|
else if(mState.canChangeTo(state))
|
||||||
{
|
{
|
||||||
|
if(mActiveStates.contains(state))
|
||||||
|
{
|
||||||
|
updateFadeTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
switch(state)
|
switch(state)
|
||||||
{
|
{
|
||||||
case ACTIVE:
|
case ACTIVE:
|
||||||
mState = state;
|
mState = state;
|
||||||
updateFadeTimeout();
|
|
||||||
broadcast(ChannelStateIdentifier.ACTIVE);
|
broadcast(ChannelStateIdentifier.ACTIVE);
|
||||||
break;
|
break;
|
||||||
case CONTROL:
|
case CONTROL:
|
||||||
@@ -111,23 +127,19 @@ public class StateMachine
|
|||||||
if(mChannelType == Channel.ChannelType.STANDARD)
|
if(mChannelType == Channel.ChannelType.STANDARD)
|
||||||
{
|
{
|
||||||
mState = state;
|
mState = state;
|
||||||
updateFadeTimeout();
|
|
||||||
broadcast(ChannelStateIdentifier.CONTROL);
|
broadcast(ChannelStateIdentifier.CONTROL);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DATA:
|
case DATA:
|
||||||
mState = state;
|
mState = state;
|
||||||
updateFadeTimeout();
|
|
||||||
broadcast(ChannelStateIdentifier.DATA);
|
broadcast(ChannelStateIdentifier.DATA);
|
||||||
break;
|
break;
|
||||||
case ENCRYPTED:
|
case ENCRYPTED:
|
||||||
mState = state;
|
mState = state;
|
||||||
updateFadeTimeout();
|
|
||||||
broadcast(ChannelStateIdentifier.ENCRYPTED);
|
broadcast(ChannelStateIdentifier.ENCRYPTED);
|
||||||
break;
|
break;
|
||||||
case CALL:
|
case CALL:
|
||||||
mState = state;
|
mState = state;
|
||||||
updateFadeTimeout();
|
|
||||||
broadcast(ChannelStateIdentifier.CALL);
|
broadcast(ChannelStateIdentifier.CALL);
|
||||||
break;
|
break;
|
||||||
case FADE:
|
case FADE:
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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.controller;
|
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.MapPanel;
|
||||||
import io.github.dsheirer.map.MapService;
|
import io.github.dsheirer.map.MapService;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.record.RecorderManager;
|
|
||||||
import io.github.dsheirer.settings.SettingsManager;
|
import io.github.dsheirer.settings.SettingsManager;
|
||||||
import io.github.dsheirer.source.SourceManager;
|
import io.github.dsheirer.source.SourceManager;
|
||||||
import io.github.dsheirer.source.tuner.TunerModel;
|
import io.github.dsheirer.source.tuner.TunerModel;
|
||||||
@@ -67,8 +63,7 @@ public class ControllerPanel extends JPanel
|
|||||||
public ControllerPanel(AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel, BroadcastModel broadcastModel,
|
public ControllerPanel(AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel, BroadcastModel broadcastModel,
|
||||||
ChannelModel channelModel, ChannelMapModel channelMapModel, ChannelProcessingManager channelProcessingManager,
|
ChannelModel channelModel, ChannelMapModel channelMapModel, ChannelProcessingManager channelProcessingManager,
|
||||||
IconManager iconManager, MapService mapService, SettingsManager settingsManager,
|
IconManager iconManager, MapService mapService, SettingsManager settingsManager,
|
||||||
SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences,
|
SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences)
|
||||||
RecorderManager recorderManager)
|
|
||||||
{
|
{
|
||||||
mBroadcastModel = broadcastModel;
|
mBroadcastModel = broadcastModel;
|
||||||
mChannelModel = channelModel;
|
mChannelModel = channelModel;
|
||||||
@@ -88,7 +83,7 @@ public class ControllerPanel extends JPanel
|
|||||||
|
|
||||||
mAliasController = new AliasController(aliasModel, broadcastModel, iconManager, userPreferences);
|
mAliasController = new AliasController(aliasModel, broadcastModel, iconManager, userPreferences);
|
||||||
|
|
||||||
mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences, recorderManager);
|
mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences);
|
||||||
|
|
||||||
init();
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * it under the terms of the GNU General Public License as published by
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
package io.github.dsheirer.controller.channel;
|
package io.github.dsheirer.controller.channel;
|
||||||
|
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
import io.github.dsheirer.alias.AliasModel;
|
||||||
|
import io.github.dsheirer.audio.AudioSegment;
|
||||||
import io.github.dsheirer.channel.IChannelDescriptor;
|
import io.github.dsheirer.channel.IChannelDescriptor;
|
||||||
import io.github.dsheirer.channel.metadata.ChannelMetadata;
|
import io.github.dsheirer.channel.metadata.ChannelMetadata;
|
||||||
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
|
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.module.log.EventLogManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.record.RecorderFactory;
|
import io.github.dsheirer.record.RecorderFactory;
|
||||||
import io.github.dsheirer.record.RecorderManager;
|
|
||||||
import io.github.dsheirer.sample.Broadcaster;
|
import io.github.dsheirer.sample.Broadcaster;
|
||||||
import io.github.dsheirer.sample.Listener;
|
import io.github.dsheirer.sample.Listener;
|
||||||
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
|
|
||||||
import io.github.dsheirer.source.Source;
|
import io.github.dsheirer.source.Source;
|
||||||
|
import io.github.dsheirer.source.SourceEvent;
|
||||||
import io.github.dsheirer.source.SourceException;
|
import io.github.dsheirer.source.SourceException;
|
||||||
import io.github.dsheirer.source.SourceManager;
|
import io.github.dsheirer.source.SourceManager;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -70,14 +70,13 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
|
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
|
||||||
private Map<Channel,ProcessingChain> mProcessingChains = new HashMap<>();
|
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 List<Listener<IDecodeEvent>> mDecodeEventListeners = new CopyOnWriteArrayList<>();
|
||||||
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
|
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
|
||||||
|
|
||||||
private ChannelMapModel mChannelMapModel;
|
private ChannelMapModel mChannelMapModel;
|
||||||
private ChannelMetadataModel mChannelMetadataModel;
|
private ChannelMetadataModel mChannelMetadataModel;
|
||||||
private EventLogManager mEventLogManager;
|
private EventLogManager mEventLogManager;
|
||||||
private RecorderManager mRecorderManager;
|
|
||||||
private SourceManager mSourceManager;
|
private SourceManager mSourceManager;
|
||||||
private AliasModel mAliasModel;
|
private AliasModel mAliasModel;
|
||||||
private UserPreferences mUserPreferences;
|
private UserPreferences mUserPreferences;
|
||||||
@@ -87,18 +86,15 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
*
|
*
|
||||||
* @param channelMapModel containing channel maps defined by the user
|
* @param channelMapModel containing channel maps defined by the user
|
||||||
* @param eventLogManager for adding event loggers to channels
|
* @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 sourceManager for obtaining a tuner channel source for the channel
|
||||||
* @param aliasModel for aliasing of identifiers produced by the channel
|
* @param aliasModel for aliasing of identifiers produced by the channel
|
||||||
* @param userPreferences for user defined behavior and settings
|
* @param userPreferences for user defined behavior and settings
|
||||||
*/
|
*/
|
||||||
public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager,
|
public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager,
|
||||||
RecorderManager recorderManager, SourceManager sourceManager,
|
SourceManager sourceManager, AliasModel aliasModel, UserPreferences userPreferences)
|
||||||
AliasModel aliasModel, UserPreferences userPreferences)
|
|
||||||
{
|
{
|
||||||
mChannelMapModel = channelMapModel;
|
mChannelMapModel = channelMapModel;
|
||||||
mEventLogManager = eventLogManager;
|
mEventLogManager = eventLogManager;
|
||||||
mRecorderManager = recorderManager;
|
|
||||||
mSourceManager = sourceManager;
|
mSourceManager = sourceManager;
|
||||||
mAliasModel = aliasModel;
|
mAliasModel = aliasModel;
|
||||||
mUserPreferences = userPreferences;
|
mUserPreferences = userPreferences;
|
||||||
@@ -252,9 +248,9 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
mChannelEventBroadcaster.addListener(processingChain);
|
mChannelEventBroadcaster.addListener(processingChain);
|
||||||
|
|
||||||
/* Register global listeners */
|
/* Register global listeners */
|
||||||
for(Listener<ReusableAudioPacket> listener : mAudioPacketListeners)
|
for(Listener<AudioSegment> listener : mAudioSegmentListeners)
|
||||||
{
|
{
|
||||||
processingChain.addAudioPacketListener(listener);
|
processingChain.addAudioSegmentListener(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Listener<IDecodeEvent> listener : mDecodeEventListeners)
|
for(Listener<IDecodeEvent> listener : mDecodeEventListeners)
|
||||||
@@ -262,6 +258,30 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
processingChain.addDecodeEventListener(listener);
|
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
|
//Register this manager to receive channel events from traffic channel manager modules within
|
||||||
//the processing chain
|
//the processing chain
|
||||||
processingChain.addChannelEventListener(this);
|
processingChain.addChannelEventListener(this);
|
||||||
@@ -289,7 +309,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Add recorders
|
//Add recorders
|
||||||
processingChain.addModules(RecorderFactory.getRecorders(mRecorderManager, mUserPreferences, channel));
|
processingChain.addModules(RecorderFactory.getRecorders(mUserPreferences, channel));
|
||||||
|
|
||||||
//Set the samples source
|
//Set the samples source
|
||||||
processingChain.setSource(source);
|
processingChain.setSource(source);
|
||||||
@@ -398,13 +418,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
|||||||
*/
|
*/
|
||||||
public void shutdown()
|
public void shutdown()
|
||||||
{
|
{
|
||||||
mLog.debug("Stopping Channels ...");
|
|
||||||
|
|
||||||
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
|
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
|
||||||
|
|
||||||
for(Channel channel : channelsToStop)
|
for(Channel channel : channelsToStop)
|
||||||
{
|
{
|
||||||
mLog.debug("Stopping channel: " + channel.toString());
|
|
||||||
stopProcessing(channel, true);
|
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
|
* Adds a message listener that will be added to all channels to receive
|
||||||
* any messages.
|
* any messages.
|
||||||
*/
|
*/
|
||||||
public void addAudioPacketListener(Listener<ReusableAudioPacket> listener)
|
public void addAudioSegmentListener(Listener<AudioSegment> listener)
|
||||||
{
|
{
|
||||||
mAudioPacketListeners.add(listener);
|
mAudioSegmentListeners.add(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes a message 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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -62,7 +63,7 @@ import java.util.List;
|
|||||||
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
|
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
|
||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
|
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;
|
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
|
//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--;
|
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;
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * 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());
|
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).
|
* 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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
* *
|
||||||
* (at your option) any later version.
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.dsp.fsk;
|
||||||
|
|
||||||
@@ -139,7 +141,7 @@ public class LTRDecoder implements Listener<ReusableFloatBuffer>, ISyncDetectLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
//no-op
|
//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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.dsp.fsk;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
@@ -129,7 +135,7 @@ public class SynchronizationMonitor implements ISyncDetectListener
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
//no-op
|
//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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.dsp.psk.pll;
|
||||||
|
|
||||||
import io.github.dsheirer.sample.complex.Complex;
|
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 mDamping = Math.sqrt(2.0) / 2.0;
|
||||||
private double mAlphaGain;
|
private double mAlphaGain;
|
||||||
private double mBetaGain;
|
private double mBetaGain;
|
||||||
private PLLGain mPLLGain = PLLGain.LEVEL_1;
|
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
|
||||||
private double mSymbolRate;
|
private double mSymbolRate;
|
||||||
private double mSampleRate;
|
private double mSampleRate;
|
||||||
private IFrequencyErrorProcessor mFrequencyErrorProcessor;
|
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
|
* Registers the listener to receive the measured frequency error as tracked by the PLL once per second
|
||||||
* second intervals.
|
|
||||||
*
|
*
|
||||||
* @param listener to receive frequency error measurement.
|
* @param listener to receive frequency error measurement.
|
||||||
*/
|
*/
|
||||||
@@ -102,22 +107,25 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
*/
|
*/
|
||||||
private void updateLoopBandwidth()
|
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));
|
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));
|
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.
|
* gain values in synchronizing with the signal carrier.
|
||||||
*
|
*
|
||||||
* @param PLLGain
|
* @param pllBandwidth
|
||||||
*/
|
*/
|
||||||
public void setPLLGain(PLLGain PLLGain)
|
public void setPLLBandwidth(PLLBandwidth pllBandwidth)
|
||||||
{
|
{
|
||||||
mPLLGain = PLLGain;
|
if(mPLLBandwidth != pllBandwidth)
|
||||||
updateLoopBandwidth();
|
{
|
||||||
|
mPLLBandwidth = pllBandwidth;
|
||||||
|
updateLoopBandwidth();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,8 +170,7 @@ public class CostasLoop implements IPhaseLockedLoop
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the costas loop frequency and phase to adjust for the phase
|
* Updates the costas loop frequency and phase to adjust for the phase error value
|
||||||
* error value
|
|
||||||
*
|
*
|
||||||
* @param phaseError - (-)= late and (+)= early
|
* @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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * it under the terms of the GNU General Public License as published by
|
||||||
@@ -21,37 +21,45 @@
|
|||||||
*/
|
*/
|
||||||
package io.github.dsheirer.dsp.psk.pll;
|
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
|
* 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),
|
BW_400(400.0, 0, 1),
|
||||||
LEVEL_1(150.0, 0, 1),
|
BW_300(300.0, 2, 3),
|
||||||
LEVEL_2(170.0, 2, 4),
|
BW_250(250.0, 4, 5),
|
||||||
LEVEL_3(190.0, 5, 6),
|
BW_200(200.0, 6, 7);
|
||||||
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);
|
|
||||||
|
|
||||||
private double mLoopBandwidth;
|
private double mLoopBandwidth;
|
||||||
private int mRangeStart;
|
private int mRangeStart;
|
||||||
private int mRangeEnd;
|
private int mRangeEnd;
|
||||||
|
|
||||||
PLLGain(double loopBandwidth, int start, int end)
|
PLLBandwidth(double loopBandwidth, int start, int end)
|
||||||
{
|
{
|
||||||
mLoopBandwidth = loopBandwidth;
|
mLoopBandwidth = loopBandwidth;
|
||||||
mRangeStart = start;
|
mRangeStart = start;
|
||||||
mRangeEnd = end;
|
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
|
* Loop bandwidth setting
|
||||||
*/
|
*/
|
||||||
@@ -76,27 +84,13 @@ public enum PLLGain
|
|||||||
* @param syncCount 0-10
|
* @param syncCount 0-10
|
||||||
* @return PLL gain appropriate for the sync detection value
|
* @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;
|
return LOOKUP_MAP.get(syncCount);
|
||||||
}
|
|
||||||
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 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;
|
package io.github.dsheirer.dsp.symbol;
|
||||||
|
|
||||||
public enum Dibit
|
public enum Dibit
|
||||||
@@ -133,6 +155,89 @@ public enum Dibit
|
|||||||
return Dibit.D00_PLUS_1;
|
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)
|
public static void main(String[] args)
|
||||||
{
|
{
|
||||||
byte a = (byte)0x1B;
|
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
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
* * it under the terms of the GNU General Public License as published by
|
* * 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_90_CW( 0x001050551155l ),
|
||||||
P25_PHASE1_ERROR_180( 0xAA8A0A008800l ),
|
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;
|
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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.dsp.symbol;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +37,8 @@ public interface ISyncDetectListener
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that sync has been lost
|
* 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()
|
public void shutdown()
|
||||||
{
|
{
|
||||||
@@ -84,6 +84,8 @@ public class JavaFxWindowManager
|
|||||||
{
|
{
|
||||||
Stage stage = mPreferencesEditor.getStage();
|
Stage stage = mPreferencesEditor.getStage();
|
||||||
stage.show();
|
stage.show();
|
||||||
|
stage.toFront();
|
||||||
|
stage.requestFocus();
|
||||||
mPreferencesEditor.showEditor(request);
|
mPreferencesEditor.showEditor(request);
|
||||||
}
|
}
|
||||||
catch(Throwable t)
|
catch(Throwable t)
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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;
|
package io.github.dsheirer.gui;
|
||||||
|
|
||||||
import com.jidesoft.plaf.LookAndFeelFactory;
|
import com.jidesoft.plaf.LookAndFeelFactory;
|
||||||
import com.jidesoft.swing.JideSplitPane;
|
import com.jidesoft.swing.JideSplitPane;
|
||||||
import io.github.dsheirer.alias.AliasModel;
|
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.BroadcastModel;
|
||||||
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
|
||||||
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
|
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.playlist.PlaylistManager;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
import io.github.dsheirer.properties.SystemProperties;
|
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.sample.Listener;
|
||||||
import io.github.dsheirer.settings.SettingsManager;
|
import io.github.dsheirer.settings.SettingsManager;
|
||||||
import io.github.dsheirer.source.SourceManager;
|
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.ThreadPool;
|
||||||
import io.github.dsheirer.util.TimeStamp;
|
import io.github.dsheirer.util.TimeStamp;
|
||||||
import jiconfont.icons.font_awesome.FontAwesome;
|
import jiconfont.icons.font_awesome.FontAwesome;
|
||||||
|
import jiconfont.javafx.IconFontFX;
|
||||||
import jiconfont.swing.IconFontSwing;
|
import jiconfont.swing.IconFontSwing;
|
||||||
import net.miginfocom.swing.MigLayout;
|
import net.miginfocom.swing.MigLayout;
|
||||||
import org.slf4j.Logger;
|
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 static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
|
||||||
|
|
||||||
private boolean mBroadcastStatusVisible;
|
private boolean mBroadcastStatusVisible;
|
||||||
private AudioPacketManager mAudioPacketManager;
|
private AudioRecordingManager mAudioRecordingManager;
|
||||||
|
private AudioStreamingManager mAudioStreamingManager;
|
||||||
private IconManager mIconManager;
|
private IconManager mIconManager;
|
||||||
private BroadcastStatusPanel mBroadcastStatusPanel;
|
private BroadcastStatusPanel mBroadcastStatusPanel;
|
||||||
private BroadcastModel mBroadcastModel;
|
private BroadcastModel mBroadcastModel;
|
||||||
@@ -164,6 +164,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
|
|
||||||
//Register FontAwesome so we can use the fonts in Swing windows
|
//Register FontAwesome so we can use the fonts in Swing windows
|
||||||
IconFontSwing.register(FontAwesome.getIconFont());
|
IconFontSwing.register(FontAwesome.getIconFont());
|
||||||
|
IconFontFX.register(FontAwesome.getIconFont());
|
||||||
|
|
||||||
TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
|
TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
|
||||||
TunerModel tunerModel = new TunerModel(tunerConfigurationModel);
|
TunerModel tunerModel = new TunerModel(tunerConfigurationModel);
|
||||||
@@ -178,16 +179,12 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
|
|
||||||
ChannelMapModel channelMapModel = new ChannelMapModel();
|
ChannelMapModel channelMapModel = new ChannelMapModel();
|
||||||
|
|
||||||
EventLogManager eventLogManager = new EventLogManager(mUserPreferences);
|
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
|
||||||
|
|
||||||
RecorderManager recorderManager = new RecorderManager(aliasModel, mUserPreferences);
|
|
||||||
|
|
||||||
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences);
|
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences);
|
||||||
|
|
||||||
mSourceManager = new SourceManager(tunerModel, mSettingsManager, mUserPreferences);
|
mSourceManager = new SourceManager(tunerModel, mSettingsManager, mUserPreferences);
|
||||||
|
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, mSourceManager,
|
||||||
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, recorderManager,
|
aliasModel, mUserPreferences);
|
||||||
mSourceManager, aliasModel, mUserPreferences);
|
|
||||||
|
|
||||||
mChannelModel.addListener(mChannelProcessingManager);
|
mChannelModel.addListener(mChannelProcessingManager);
|
||||||
mChannelProcessingManager.addChannelEventListener(mChannelModel);
|
mChannelProcessingManager.addChannelEventListener(mChannelModel);
|
||||||
@@ -195,25 +192,26 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
ChannelSelectionManager channelSelectionManager = new ChannelSelectionManager(mChannelModel);
|
ChannelSelectionManager channelSelectionManager = new ChannelSelectionManager(mChannelModel);
|
||||||
mChannelModel.addListener(channelSelectionManager);
|
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);
|
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
|
mChannelProcessingManager.addAudioSegmentListener(audioPlaybackManager);
|
||||||
//distributed to the audio packet processors (ie playback, recording, streaming, etc.)
|
mChannelProcessingManager.addAudioSegmentListener(mAudioRecordingManager);
|
||||||
mAudioPacketManager = new AudioPacketManager(aliasModel);
|
mChannelProcessingManager.addAudioSegmentListener(mAudioStreamingManager);
|
||||||
mAudioPacketManager.addListener(recorderManager);
|
|
||||||
mAudioPacketManager.addListener(audioPlaybackManager);
|
|
||||||
mAudioPacketManager.addListener(mBroadcastModel);
|
|
||||||
mAudioPacketManager.start();
|
|
||||||
mChannelProcessingManager.addAudioPacketListener(mAudioPacketManager);
|
|
||||||
|
|
||||||
MapService mapService = new MapService(mIconManager);
|
MapService mapService = new MapService(mIconManager);
|
||||||
mChannelProcessingManager.addDecodeEventListener(mapService);
|
mChannelProcessingManager.addDecodeEventListener(mapService);
|
||||||
|
|
||||||
mControllerPanel = new ControllerPanel(audioPlaybackManager, aliasModel, mBroadcastModel,
|
mControllerPanel = new ControllerPanel(audioPlaybackManager, aliasModel, mBroadcastModel,
|
||||||
mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager,
|
mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager,
|
||||||
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences, recorderManager);
|
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences);
|
||||||
|
|
||||||
mSpectralPanel = new SpectralDisplayPanel(mChannelModel,
|
mSpectralPanel = new SpectralDisplayPanel(mChannelModel,
|
||||||
mChannelProcessingManager, mSettingsManager, tunerModel);
|
mChannelProcessingManager, mSettingsManager, tunerModel);
|
||||||
@@ -411,7 +409,7 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
viewMenu.add(new JSeparator());
|
viewMenu.add(new JSeparator());
|
||||||
JMenuItem preferencesItem = new JMenuItem("Preferences");
|
JMenuItem preferencesItem = new JMenuItem("Preferences");
|
||||||
preferencesItem.addActionListener(e -> MyEventBus.getEventBus()
|
preferencesItem.addActionListener(e -> MyEventBus.getEventBus()
|
||||||
.post(new PreferenceEditorViewRequest(PreferenceEditorType.CHANNEL_EVENT)));
|
.post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK)));
|
||||||
viewMenu.add(preferencesItem);
|
viewMenu.add(preferencesItem);
|
||||||
|
|
||||||
menuBar.add(viewMenu);
|
menuBar.add(viewMenu);
|
||||||
@@ -471,10 +469,10 @@ public class SDRTrunk implements Listener<TunerEvent>
|
|||||||
mJavaFxWindowManager.shutdown();
|
mJavaFxWindowManager.shutdown();
|
||||||
mLog.info("Stopping channels ...");
|
mLog.info("Stopping channels ...");
|
||||||
mChannelProcessingManager.shutdown();
|
mChannelProcessingManager.shutdown();
|
||||||
mAudioPacketManager.stop();
|
mAudioRecordingManager.stop();
|
||||||
|
|
||||||
mLog.info("Stopping spectral display ...");
|
mLog.info("Stopping spectral display ...");
|
||||||
mSpectralPanel.clearTuner();
|
mSpectralPanel.clearTuner();
|
||||||
mLog.info("Releasing tuners ...");
|
|
||||||
mSourceManager.shutdown();
|
mSourceManager.shutdown();
|
||||||
mLog.info("Shutdown complete.");
|
mLog.info("Shutdown complete.");
|
||||||
mApplicationLog.stop();
|
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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.gui.instrument.decoder;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
@@ -46,7 +52,7 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
//no-op
|
//no-op
|
||||||
}
|
}
|
||||||
@@ -60,7 +66,7 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
//no-op
|
//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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.gui.instrument.decoder;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
@@ -46,7 +52,7 @@ public class MDC1200Pane extends AbstractAFSK1200Pane
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
mLog.debug("Sync Lost!");
|
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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.gui.instrument.decoder;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
@@ -46,7 +52,7 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
//no-op
|
//no-op
|
||||||
}
|
}
|
||||||
@@ -60,7 +66,7 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
//no-op
|
//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
|
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* later version.
|
* *
|
||||||
|
* * This program is free software: you can redistribute it and/or modify
|
||||||
|
* * it under the terms of the GNU General Public License as published by
|
||||||
|
* * the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* * (at your option) any later version.
|
||||||
|
* *
|
||||||
|
* * This program is distributed in the hope that it will be useful,
|
||||||
|
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* * GNU General Public License for more details.
|
||||||
|
* *
|
||||||
|
* * 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;
|
package io.github.dsheirer.gui.instrument.decoder;
|
||||||
|
|
||||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||||
@@ -46,7 +52,7 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -60,7 +66,7 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncLost()
|
public void syncLost(int bitsProcessed)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.gui.preference;
|
package io.github.dsheirer.gui.preference;
|
||||||
|
|
||||||
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
|
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
|
||||||
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
|
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.ChannelMultipleFrequencyPreferenceEditor;
|
||||||
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
|
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
|
||||||
import io.github.dsheirer.preference.UserPreferences;
|
import io.github.dsheirer.preference.UserPreferences;
|
||||||
@@ -36,18 +37,22 @@ public class PreferenceEditorFactory
|
|||||||
{
|
{
|
||||||
switch(preferenceEditorType)
|
switch(preferenceEditorType)
|
||||||
{
|
{
|
||||||
|
case AUDIO_PLAYBACK:
|
||||||
|
return new PlaybackPreferenceEditor(userPreferences);
|
||||||
|
case AUDIO_RECORD:
|
||||||
|
return new RecordPreferenceEditor(userPreferences);
|
||||||
case CHANNEL_EVENT:
|
case CHANNEL_EVENT:
|
||||||
return new DecodeEventViewPreferenceEditor(userPreferences);
|
return new DecodeEventViewPreferenceEditor(userPreferences);
|
||||||
case JMBE_LIBRARY:
|
|
||||||
return new JmbeLibraryPreferenceEditor(userPreferences);
|
|
||||||
case DIRECTORY:
|
case DIRECTORY:
|
||||||
return new DirectoryPreferenceEditor(userPreferences);
|
return new DirectoryPreferenceEditor(userPreferences);
|
||||||
|
case JMBE_LIBRARY:
|
||||||
|
return new JmbeLibraryPreferenceEditor(userPreferences);
|
||||||
case SOURCE_CHANNEL_MULTIPLE_FREQUENCY:
|
case SOURCE_CHANNEL_MULTIPLE_FREQUENCY:
|
||||||
return new ChannelMultipleFrequencyPreferenceEditor(userPreferences);
|
return new ChannelMultipleFrequencyPreferenceEditor(userPreferences);
|
||||||
case TALKGROUP_FORMAT:
|
|
||||||
return new TalkgroupFormatPreferenceEditor(userPreferences);
|
|
||||||
case SOURCE_TUNER_CHANNELIZER:
|
case SOURCE_TUNER_CHANNELIZER:
|
||||||
return new TunerPreferenceEditor(userPreferences);
|
return new TunerPreferenceEditor(userPreferences);
|
||||||
|
case TALKGROUP_FORMAT:
|
||||||
|
return new TalkgroupFormatPreferenceEditor(userPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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.preference;
|
package io.github.dsheirer.gui.preference;
|
||||||
@@ -28,11 +25,14 @@ package io.github.dsheirer.gui.preference;
|
|||||||
public enum PreferenceEditorType
|
public enum PreferenceEditorType
|
||||||
{
|
{
|
||||||
CHANNEL_EVENT("Channel Events"),
|
CHANNEL_EVENT("Channel Events"),
|
||||||
JMBE_LIBRARY("JMBE Audio Library"),
|
|
||||||
DIRECTORY("Directories"),
|
DIRECTORY("Directories"),
|
||||||
|
JMBE_LIBRARY("JMBE Audio Library"),
|
||||||
|
AUDIO_RECORD("Record"),
|
||||||
|
AUDIO_PLAYBACK("Playback"),
|
||||||
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
|
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
|
||||||
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
|
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
|
||||||
TALKGROUP_FORMAT("Talkgroup & Radio ID");
|
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
|
||||||
|
DEFAULT("Default");
|
||||||
|
|
||||||
private String mLabel;
|
private String mLabel;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ******************************************************************************
|
* *****************************************************************************
|
||||||
* sdrtrunk
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* 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
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
* *****************************************************************************
|
* ****************************************************************************
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.github.dsheirer.gui.preference;
|
package io.github.dsheirer.gui.preference;
|
||||||
@@ -29,12 +28,13 @@ import javafx.geometry.Pos;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Control;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.SelectionMode;
|
import javafx.scene.control.SelectionMode;
|
||||||
import javafx.scene.control.TreeItem;
|
import javafx.scene.control.TreeItem;
|
||||||
import javafx.scene.control.TreeView;
|
import javafx.scene.control.TreeView;
|
||||||
import javafx.scene.layout.BorderPane;
|
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.stage.Window;
|
import javafx.stage.Window;
|
||||||
@@ -52,25 +52,19 @@ public class PreferencesEditor extends Application
|
|||||||
{
|
{
|
||||||
private final static Logger mLog = LoggerFactory.getLogger(PreferencesEditor.class);
|
private final static Logger mLog = LoggerFactory.getLogger(PreferencesEditor.class);
|
||||||
|
|
||||||
|
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
|
||||||
private UserPreferences mUserPreferences;
|
private UserPreferences mUserPreferences;
|
||||||
private BorderPane mBorderPane;
|
private HBox mParentBox;
|
||||||
private TreeView mEditorSelectionTreeView;
|
private TreeView mEditorSelectionTreeView;
|
||||||
private DecodeEventViewPreferenceEditor mDecodeEventViewPreferenceEditor;
|
private VBox mEditorAndButtonsBox;
|
||||||
private VBox mDefaultEditor;
|
private Node mEditor;
|
||||||
private HBox mControlBox;
|
private HBox mButtonsBox;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs a preferences editor instance
|
|
||||||
*/
|
|
||||||
public PreferencesEditor()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public Stage getStage()
|
public Stage getStage()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Window window = getBorderPane().getScene().getWindow();
|
Window window = getParentBox().getScene().getWindow();
|
||||||
|
|
||||||
if(window instanceof Stage)
|
if(window instanceof Stage)
|
||||||
{
|
{
|
||||||
@@ -104,7 +98,7 @@ public class PreferencesEditor extends Application
|
|||||||
public void start(Stage stage) throws Exception
|
public void start(Stage stage) throws Exception
|
||||||
{
|
{
|
||||||
stage.setTitle("Preferences");
|
stage.setTitle("Preferences");
|
||||||
Scene scene = new Scene(getBorderPane(), 900, 500);
|
Scene scene = new Scene(getParentBox(), 900, 500);
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.show();
|
stage.show();
|
||||||
}
|
}
|
||||||
@@ -130,8 +124,7 @@ public class PreferencesEditor extends Application
|
|||||||
{
|
{
|
||||||
TreeItem treeItem = li.next();
|
TreeItem treeItem = li.next();
|
||||||
|
|
||||||
if(treeItem.getValue() instanceof PreferenceEditorType &&
|
if(treeItem.getValue() instanceof PreferenceEditorType && (treeItem.getValue()).equals(type))
|
||||||
((PreferenceEditorType)treeItem.getValue()).equals(type))
|
|
||||||
{
|
{
|
||||||
return treeItem;
|
return treeItem;
|
||||||
}
|
}
|
||||||
@@ -153,31 +146,48 @@ public class PreferencesEditor extends Application
|
|||||||
/**
|
/**
|
||||||
* Primary layout for the editor window
|
* Primary layout for the editor window
|
||||||
*/
|
*/
|
||||||
private BorderPane getBorderPane()
|
private HBox getParentBox()
|
||||||
{
|
{
|
||||||
if(mBorderPane == null)
|
if(mParentBox == null)
|
||||||
{
|
{
|
||||||
mBorderPane = new BorderPane();
|
mParentBox = new HBox();
|
||||||
mBorderPane.setLeft(getEditorSelectionTreeView());
|
mParentBox.getChildren().add(getEditorSelectionTreeView());
|
||||||
mBorderPane.setCenter(getDefaultEditor());
|
HBox.setHgrow(getEditorAndButtonsBox(), Priority.ALWAYS);
|
||||||
mBorderPane.setBottom(getControlBox());
|
mParentBox.getChildren().add(getEditorAndButtonsBox());
|
||||||
}
|
}
|
||||||
|
|
||||||
return mBorderPane;
|
return mParentBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VBox getDefaultEditor()
|
private VBox getEditorAndButtonsBox()
|
||||||
{
|
{
|
||||||
if(mDefaultEditor == null)
|
if(mEditorAndButtonsBox == null)
|
||||||
{
|
{
|
||||||
mDefaultEditor = new VBox();
|
mEditorAndButtonsBox = new VBox();
|
||||||
mDefaultEditor.setPadding(new Insets(10, 10, 10, 10));
|
mEditor = getDefaultEditor();
|
||||||
Label label = new Label("Please select a preference ...");
|
VBox.setVgrow(getDefaultEditor(), Priority.ALWAYS);
|
||||||
|
VBox.setVgrow(getButtonsBox(), Priority.NEVER);
|
||||||
mDefaultEditor.getChildren().add(label);
|
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> 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");
|
TreeItem<String> decoderItem = new TreeItem<>("Decoder");
|
||||||
decoderItem.getChildren().add(new TreeItem(PreferenceEditorType.JMBE_LIBRARY));
|
decoderItem.getChildren().add(new TreeItem(PreferenceEditorType.JMBE_LIBRARY));
|
||||||
treeRoot.getChildren().add(decoderItem);
|
treeRoot.getChildren().add(decoderItem);
|
||||||
@@ -217,43 +233,56 @@ public class PreferencesEditor extends Application
|
|||||||
treeRoot.setExpanded(true);
|
treeRoot.setExpanded(true);
|
||||||
mEditorSelectionTreeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
mEditorSelectionTreeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||||
mEditorSelectionTreeView.getSelectionModel().selectedItemProperty().addListener(new EditorTreeSelectionListener());
|
mEditorSelectionTreeView.getSelectionModel().selectedItemProperty().addListener(new EditorTreeSelectionListener());
|
||||||
|
|
||||||
|
mEditorSelectionTreeView.setMinWidth(Control.USE_PREF_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mEditorSelectionTreeView;
|
return mEditorSelectionTreeView;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode event view preferences editor
|
|
||||||
*/
|
|
||||||
private DecodeEventViewPreferenceEditor getDecodeEventViewPreferenceEditor()
|
|
||||||
{
|
|
||||||
if(mDecodeEventViewPreferenceEditor == null)
|
|
||||||
{
|
|
||||||
mDecodeEventViewPreferenceEditor = new DecodeEventViewPreferenceEditor(getUserPreferences());
|
|
||||||
}
|
|
||||||
|
|
||||||
return mDecodeEventViewPreferenceEditor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Control box with OK button.
|
* 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");
|
Button okButton = new Button("Ok");
|
||||||
okButton.setOnAction(event -> {
|
okButton.setOnAction(event -> {
|
||||||
Stage stage = (Stage)getBorderPane().getScene().getWindow();
|
Stage stage = (Stage)getParentBox().getScene().getWindow();
|
||||||
stage.close();
|
stage.close();
|
||||||
});
|
});
|
||||||
HBox.setMargin(okButton, new Insets(5, 5, 5, 5));
|
HBox.setMargin(okButton, new Insets(5, 5, 5, 5));
|
||||||
mControlBox.setAlignment(Pos.CENTER_RIGHT);
|
mButtonsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
mControlBox.getChildren().add(okButton);
|
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
|
public class EditorTreeSelectionListener implements ChangeListener
|
||||||
{
|
{
|
||||||
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue observable, Object oldValue, Object newValue)
|
public void changed(ObservableValue observable, Object oldValue, Object newValue)
|
||||||
{
|
{
|
||||||
Node editor = getEditor(newValue);
|
if(newValue instanceof TreeItem)
|
||||||
BorderPane.setAlignment(editor, Pos.CENTER_LEFT);
|
|
||||||
getBorderPane().setCenter(editor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Node getEditor(Object treeNodeItem)
|
|
||||||
{
|
|
||||||
if(treeNodeItem instanceof TreeItem)
|
|
||||||
{
|
{
|
||||||
Object value = ((TreeItem)treeNodeItem).getValue();
|
Object value = ((TreeItem)newValue).getValue();
|
||||||
|
|
||||||
if(value instanceof PreferenceEditorType)
|
if(value instanceof PreferenceEditorType)
|
||||||
{
|
{
|
||||||
PreferenceEditorType type = (PreferenceEditorType)value;
|
setEditor((PreferenceEditorType)value);
|
||||||
|
return;
|
||||||
Node editor = mEditors.get(type);
|
|
||||||
|
|
||||||
if(editor != null)
|
|
||||||
{
|
|
||||||
return editor;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor = PreferenceEditorFactory.getEditor(type, getUserPreferences());
|
|
||||||
|
|
||||||
if(editor != null)
|
|
||||||
{
|
|
||||||
mEditors.put(type, editor);
|
|
||||||
return editor;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
if(mEditorPane == null)
|
||||||
{
|
{
|
||||||
mEditorPane = new GridPane();
|
mEditorPane = new GridPane();
|
||||||
|
mEditorPane.setVgap(10);
|
||||||
|
mEditorPane.setHgap(10);
|
||||||
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
|
||||||
GridPane.setMargin(getChannelizerLabel(), new Insets(0, 10, 0, 0));
|
|
||||||
GridPane.setHalignment(getChannelizerLabel(), HPos.LEFT);
|
GridPane.setHalignment(getChannelizerLabel(), HPos.LEFT);
|
||||||
mEditorPane.add(getChannelizerLabel(), 0, 0);
|
mEditorPane.add(getChannelizerLabel(), 0, 0);
|
||||||
GridPane.setMargin(getChannelizerTypeChoiceBox(), new Insets(2, 0, 2, 0));
|
|
||||||
mEditorPane.add(getChannelizerTypeChoiceBox(), 1, 0);
|
mEditorPane.add(getChannelizerTypeChoiceBox(), 1, 0);
|
||||||
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, 1, 2, 1);
|
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, 1, 2, 1);
|
||||||
mEditorPane.add(getPolyphaseLabel(), 0, 2, 2, 1);
|
mEditorPane.add(getPolyphaseLabel(), 0, 2, 2, 1);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public class Icon implements Comparable<Icon>
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(mPath.startsWith("/"))
|
if(!mPath.startsWith("images"))
|
||||||
{
|
{
|
||||||
mImageIcon = new ImageIcon(mPath);
|
mImageIcon = new ImageIcon(mPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
/*
|
/*
|
||||||
|
* *****************************************************************************
|
||||||
|
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||||
*
|
*
|
||||||
* * ******************************************************************************
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
* it under the terms of the GNU General Public License as published by
|
||||||
* *
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
* * This program is free software: you can redistribute it and/or modify
|
* (at your option) any later version.
|
||||||
* * 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.identifier;
|
package io.github.dsheirer.identifier;
|
||||||
@@ -41,7 +38,6 @@ public class IdentifierCollection
|
|||||||
private final static Logger mLog = LoggerFactory.getLogger(IdentifierCollection.class);
|
private final static Logger mLog = LoggerFactory.getLogger(IdentifierCollection.class);
|
||||||
protected List<Identifier> mIdentifiers = new ArrayList<>();
|
protected List<Identifier> mIdentifiers = new ArrayList<>();
|
||||||
protected AliasListConfigurationIdentifier mAliasListConfigurationIdentifier;
|
protected AliasListConfigurationIdentifier mAliasListConfigurationIdentifier;
|
||||||
private boolean mUpdated = false;
|
|
||||||
private int mTimeslot = 0;
|
private int mTimeslot = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,19 +90,6 @@ public class IdentifierCollection
|
|||||||
mTimeslot = timeslot;
|
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.
|
* Alias List configuration identifier containing the name of the alias list for this collection.
|
||||||
* @return alias list or null
|
* @return alias list or null
|
||||||
@@ -319,4 +302,19 @@ public class IdentifierCollection
|
|||||||
return null;
|
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)
|
private void notifyAdd(Identifier identifier)
|
||||||
{
|
{
|
||||||
setUpdated(true);
|
|
||||||
if(mListener != null)
|
if(mListener != null)
|
||||||
{
|
{
|
||||||
mListener.receive(new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, getTimeslot()));
|
mListener.receive(new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, getTimeslot()));
|
||||||
@@ -110,7 +109,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
|
|||||||
*/
|
*/
|
||||||
private void notifyRemove(Identifier identifier)
|
private void notifyRemove(Identifier identifier)
|
||||||
{
|
{
|
||||||
setUpdated(true);
|
|
||||||
if(mListener != null)
|
if(mListener != null)
|
||||||
{
|
{
|
||||||
mListener.receive(new IdentifierUpdateNotification(identifier,
|
mListener.receive(new IdentifierUpdateNotification(identifier,
|
||||||
@@ -409,8 +407,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
|
|||||||
public IdentifierCollection copyOf()
|
public IdentifierCollection copyOf()
|
||||||
{
|
{
|
||||||
IdentifierCollection copy = new IdentifierCollection(getIdentifiers(), getTimeslot());
|
IdentifierCollection copy = new IdentifierCollection(getIdentifiers(), getTimeslot());
|
||||||
copy.setUpdated(isUpdated());
|
|
||||||
setUpdated(false);
|
|
||||||
return copy;
|
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