Compare commits

..

45 Commits

Author SHA1 Message Date
Denny Sheirer
2e37f1e687 Updates gradle wrapper to 6.3. Updates embeded java to JDK 14 for building platform releases. (#729)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-04 09:22:13 -04:00
Denny Sheirer
5e8f2d0523 Updates build to 0.4.0 final and updates CHANGELOG revision history. (#727)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-04 08:19:29 -04:00
crsawyer
9fb6c33319 Fix HackRF-JawBreaker and HackRF sample conversion (#725) 2020-04-04 08:02:17 -04:00
Denny Sheirer
fdecac4230 Resovles #721 Suppress RID values of 0. (#723)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-01 05:05:28 -04:00
Denny Sheirer
85c27a6cca Resolves #719 P25 Phase 2 display channel number (#722)
Co-authored-by: Denny <denny@denny-desktop>
2020-04-01 03:28:26 -04:00
Denny Sheirer
57f56d31a2 #713 Update Version 0.4.0 Beta 8 (#714)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 12:06:23 -04:00
Denny Sheirer
df25675d3f Resolves #707 separate DTMF tones of the same value get merged together (#712)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 12:03:47 -04:00
Denny Sheirer
277a84702a Resolves #580 channel map selection and editing (#711)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 11:52:39 -04:00
Denny Sheirer
c651296378 Resolves #709 MPT1327 no audio on traffic channels. (#710)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 11:33:15 -04:00
Denny Sheirer
866d25e86a #705 Updates release version 0.4.0 beta 7 (#706)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 06:17:06 -04:00
Denny Sheirer
5e62f1b889 Resolves #677 Streaming Audio Metadata UNKNOWN/UNKNOWN (#704)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 05:58:21 -04:00
Denny Sheirer
e9d524ad14 Resolves #699 Channel Multi-Frequency rotation delay user preference does not persist. (#703)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-28 05:53:58 -04:00
Denny Sheirer
158c8b4b8a #701 P25 Phase 2 Tone Metadata and Channel State (#702)
* Resolves #701 Updates P25P2 Tone metadata handling and P25P2 channel state management.
2020-03-28 05:51:11 -04:00
Denny Sheirer
41df207bb9 Resolves #694. Address null pointer when no system audio devices are available. (#695)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-23 04:45:06 -04:00
Denny Sheirer
692ffabe82 Resolves #692. Updates Java requirement to version 13 and JavaFX libraries to version 13. (#693)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-22 09:30:43 -04:00
Denny
944ad5a571 Updates version to 0.4.0 beta 6 2020-03-22 06:39:53 -04:00
Denny
5ca70d3940 Removes logging from invalid channel type values. 2020-03-22 06:07:36 -04:00
Denny Sheirer
904752e3dd Resolves #688. Updates HackRF Sample Rate options and sets usable bandwidth for the tuner to 75% of the sample rate. (#689)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-22 05:31:56 -04:00
Denny Sheirer
cf61798466 Resolves #685. Resolves several issues with P25 Phase 2 channel state and call handling. (#687)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-21 14:42:04 -04:00
Denny Sheirer
fd72f459f9 Resolves #685. Resolves several issues with P25 Phase 2 channel state and call handling. (#686)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-21 14:31:26 -04:00
Denny Sheirer
1bc462b300 Resolves #676 Do Not Monitor feature not working. (#684)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-21 05:32:12 -04:00
Denny Sheirer
1c6f91f101 Resolves #682 NPE when logging channel events with no alias list specified. (#683)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-20 11:15:53 -04:00
Denny
a7c28f066a Resolved NPE with tone updates. 2020-03-20 09:14:10 -04:00
Denny Sheirer
604c8ab6e5 Resolves #680 Audio Do Not Monitor is not working. Adds audio segment drop tone to indicate that an audio playback is dropped because the do not monitor setting was applied after the audio segment started. (#681)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-20 08:31:35 -04:00
Denny Sheirer
23a3ee39cc Resolves #678 AudioChannelPanel threading issue. (#679)
Co-authored-by: Denny <denny@denny-desktop>
2020-03-20 05:21:49 -04:00
Denny Sheirer
fa16db129b #660 Updates audio processing subsystem to use audio segments. Updates audio playback, recording and streaming to use audio segments. Adds support to UserPreferences for sound card selection and audio tone insertion during playback. Adds record preference for WAVE or MP3. Resolves issues with metadata in recordings. (#675) 2020-03-19 06:35:05 -04:00
Denny
de249c448b Removes debug logging. 2020-02-29 11:02:32 +00:00
Denny
e1bc56c500 Resolves issue with build script where OS-specific JavaFX libraries were not included in build images. 2020-02-23 08:35:49 -05:00
Denny Sheirer
d1d115d11f Updates build to version 0.4.0 beta 5 (#668) 2020-02-23 09:49:00 +00:00
Allegiance Zone pusher
aca41b4f6e alias in event log.diff (#659) 2020-02-22 20:38:37 +00:00
whelgeson
1e3ae996ac Implementing some build script cleanup work. (#664)
- Adding jitpack to import libs directly from the git repos
 - Removing binary imports from repo
 - Fixing Gradle 7 upgrade warnings
2020-02-22 20:31:29 +00:00
Denny Sheirer
b3b2e74688 Resolves #663. Updates P25 phase 2 Decoder to detect PLL lock errors for +/-90 and 180 out-of-phase. Adds support for broadcasting phase 2 channel frequency error calculations to support tuner auto-correction for frequency errors. Removes adaptive PLL bandwidth support from Phase 1 and uses a fixed PLL bandwidth for both phase 1 and phase 2 decoders. Updates the Phase 2 symbol timing gain with results from extensive testing of PLL bandwidth and symbol timing combinations. (#666) 2020-02-22 20:09:24 +00:00
Denny Sheirer
44237bec5c Resolves issue where user added icons were not displaying on windows OS. (#658) 2020-02-16 12:22:27 +00:00
Denny Sheirer
53b44f44e3 Updates gradle build to use version-specific root folder names for OS-specific image products. (#657) 2020-02-16 11:50:20 +00:00
Denny
9af7a184ad Utility for calculating PLL phase lock sync pattern derivatives 2020-02-14 06:11:15 -05:00
Denny Sheirer
9c8d87ec64 #651 Updates audio broadcaster to decrement pending replay count so that pending recordings don't cause a memory leak when the streaming server is not available. (#656) 2020-02-09 10:54:46 -05:00
Denny Sheirer
4c34968d28 #654 Warn users when USB bus speeds are exceeded. (#655)
Updates logging to track max usb speeds per bus and warns user when any USB bus combined tuner data rates exceeds 70% of the max bus rate.
2020-02-09 10:51:55 -05:00
Denny Sheirer
2d71f5173a #652 USB Tuner Error Handling Enhancements (#653)
-Separates USB timeout event processing to a dedicated thread
-Updates USB transfer buffer processor to better detect errors and auto-restart on error (max 3 times)
-Updates Tuner to add an error state and SourceManager to not select tuners with an error state
-Updates ChannelProcessingManager to auto-shutdown channels sourced from tuners set with error state
-Improves logging of USB related errors.
2020-02-09 05:40:04 -05:00
Denny Sheirer
2e942113c6 Resolves #644 NoClassDefFound error when Icecast metadata updater runs. Updates build to version 0.4.0 Beta 4 (#645) 2020-01-12 04:57:38 -05:00
Denny Sheirer
e20540ccc6 #630 Update gradle build to allow runtime image builds without locally installed JDKs. Updates build version to 0.4.0 beta 3 (#643) 2020-01-11 07:01:17 -05:00
Denny Sheirer
8d7e0e9f46 #641 Updates P25 Talkgroup and Radio Id valid ranges to allow value of 0. (#642) 2020-01-11 06:13:44 -05:00
Denny Sheirer
cc5d94f58d #639 Updates all streaming metadata to include radio identifier value as the from value. (#640) 2020-01-11 06:06:08 -05:00
Denny Sheirer
26197e433c #576 P25 Activity Summary Hex and Decimal Network Values (#638)
* #576 Updates P25 activity summaries to list values in hex and decimal.  Resolves issue with P25 explicit channel uplink frequency calculations.  Resolves issue with Motorola base station id messages and traffic channel messages.  Resolves issue with RFSS link control message control channel frequency calculations.  Resolves issue with error when using recording tuners.
2020-01-11 05:33:38 -05:00
Denny Sheirer
f54b4dfd0e 576 Update P25 Activity Summary To Provide Network Values in Hex and Decimal (#637)
* #576 Updates P25 activity summaries to list values in hex and decimal.  Resolves issue with P25 explicit channel uplink frequency calculations.  Resolves issue with Motorola base station id messages and traffic channel messages.  Resolves issue with RFSS link control message control channel frequency calculations.  Resolves issue with error when using recording tuners.
2020-01-11 05:23:47 -05:00
Denny Sheirer
085ba52294 #632 Resolves issue with audio recordings and from identifiers stemming from changes to the way radio IDs are handled. (#634) 2020-01-05 15:48:47 -05:00
346 changed files with 8856 additions and 6700 deletions

11
.idea/compiler.xml generated
View File

@@ -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>

View File

@@ -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
View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="11" project-jdk-type="JavaSDK">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_X" default="true" project-jdk-name="14" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

12
.idea/modules.xml generated
View File

@@ -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
View File

@@ -1,16 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="IssueNavigationConfiguration">
<option name="links">
<list>
<IssueNavigationLink>
<option name="issueRegexp" value="#(\d+)" />
<option name="linkRegexp" value="https://github.com/DSheirer/sdrtrunk/issues/$1" />
</IssueNavigationLink>
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@@ -1,3 +1,18 @@
0.4.0 20200404
- Release includes embedded Java 14 & JavaFX 13 runtime
- Adds Polyphase Channelizer with 2x channel oversampling and redesigned source management
- Adds P25 Phase 2 decoder
- Adds P25 radio identifier aliasing
- JMBE library 1.0.4 with support for AMBE tone metadata
- New JavaFX User Preferences dialog
- HackRF tuner enhanced (sample rates and saturation issues)
- Adds support for Airspy mini tuner
- Redesigned audio sub-system that uses AudioSegments
- USB tuner bus saturation management/monitoring
- Bitstream (.bits) recording
- MP3 Audio Recording
- Numerous bug fixes
0.3.4 20180418
- New P25 C4FM decoder that uses Differential QPSK decoder instead of FM demodulation with 4FSK.
- Adds channel auto-start and ordering capability with startup dialog allowing user to invoke or bypass channel

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -23,17 +23,18 @@ plugins {
id 'application'
id 'java'
id 'idea'
id 'org.openjfx.javafxplugin' version '0.0.6'
id 'org.openjfx.javafxplugin' version '0.0.8'
id 'org.beryx.runtime' version '1.8.0'
}
repositories {
jcenter()
mavenCentral()
maven { url "https://jitpack.io" }
}
version = '0.4.0-beta.2'
sourceCompatibility = '11'
version = '0.4.0'
sourceCompatibility = '13'
sourceSets {
main.java.srcDirs 'src/main'
@@ -41,45 +42,56 @@ sourceSets {
}
dependencies {
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:3.8.0'
compile fileTree(dir: 'imports', include: '*.jar')
compile 'ch.qos.logback:logback-core:1.2.3'
compile 'ch.qos.logback:logback-classic:1.2.3'
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
compile 'com.fazecast:jSerialComm:2.5.0'
compile 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
compile 'com.github.jiconfont:jiconfont-javafx:1.0.0'
compile 'com.github.jiconfont:jiconfont-swing:1.0.1'
compile 'com.github.wendykierp:JTransforms:3.1'
compile 'com.google.guava:guava:27.0.1-jre'
compile 'com.jidesoft:jide-oss:3.6.18'
compile 'com.miglayout:miglayout-swing:5.2'
compile 'eu.hansolo:charts:1.0.5'
compile 'io.github.dsheirer:radio-reference-api:15.1.2'
compile 'javax.usb:usb-api:1.0.2'
compile 'net.coderazzi:tablefilter-swing:5.4.0'
compile 'org.apache.commons:commons-lang3:3.8.1'
compile 'org.apache.commons:commons-io:1.3.2'
compile 'org.apache.mina:mina-core:2.0.19'
compile 'org.apache.mina:mina-http:2.0.19'
compile "org.openjfx:javafx-base:11:win"
compile "org.openjfx:javafx-base:11:linux"
compile "org.openjfx:javafx-base:11:mac"
compile "org.openjfx:javafx-controls:11.0.2:win"
compile "org.openjfx:javafx-controls:11.0.2:linux"
compile "org.openjfx:javafx-controls:11.0.2:mac"
compile "org.openjfx:javafx-graphics:11.0.2:win"
compile "org.openjfx:javafx-graphics:11.0.2:linux"
compile "org.openjfx:javafx-graphics:11.0.2:mac"
compile "org.openjfx:javafx-swing:11.0.2:win"
compile "org.openjfx:javafx-swing:11.0.2:linux"
compile "org.openjfx:javafx-swing:11.0.2:mac"
compile 'org.slf4j:slf4j-api:1.7.25'
compile 'org.usb4java:libusb4java:1.3.0'
compile 'org.usb4java:usb4java:1.3.0'
compile 'org.usb4java:usb4java-javax:1.3.0'
compile 'pl.edu.icm:JLargeArrays:1.6'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.8.0'
//Jitpack imports
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
implementation 'com.github.dnault:libresample4j:master'
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
//mavenCentral/jcenter imports
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'ch.qos.logback:logback-core:1.2.3'
implementation 'ch.qos.logback:logback-classic:1.2.3'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
implementation 'com.fazecast:jSerialComm:2.5.0'
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation 'com.github.wendykierp:JTransforms:3.1'
implementation 'com.google.guava:guava:27.0.1-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:5.2'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:radio-reference-api:15.1.2'
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.4.0'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'org.apache.commons:commons-io:1.3.2'
implementation 'org.apache.mina:mina-core:2.0.19'
implementation 'org.apache.mina:mina-http:2.0.19'
implementation 'org.controlsfx:controlsfx:11.0.1'
implementation 'org.slf4j:slf4j-api:1.7.25'
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'pl.edu.icm:JLargeArrays:1.6'
//The must remain as compile options to ensure they are included in the build to produce multi-OS images
compile "org.openjfx:javafx-base:13:win"
compile "org.openjfx:javafx-base:13:linux"
compile "org.openjfx:javafx-base:13:mac"
compile "org.openjfx:javafx-controls:13.0.1:win"
compile "org.openjfx:javafx-controls:13.0.1:linux"
compile "org.openjfx:javafx-controls:13.0.1:mac"
compile "org.openjfx:javafx-graphics:13.0.1:win"
compile "org.openjfx:javafx-graphics:13.0.1:linux"
compile "org.openjfx:javafx-graphics:13.0.1:mac"
compile "org.openjfx:javafx-swing:13.0.1:win"
compile "org.openjfx:javafx-swing:13.0.1:linux"
compile "org.openjfx:javafx-swing:13.0.1:mac"
}
application {
@@ -100,128 +112,117 @@ javafx {
jar {
manifest {
attributes (
'Implementation-Title' : 'sdrtrunk project',
'Implementation-Version' : archiveVersion,
'Class-Path' : 'jmbe-1.0.1.jar',
'Created-By' : "Gradle ${gradle.gradleVersion}",
'Build-Timestamp' : new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
'Build-OS' : "${System.properties['os.name']} (${System.properties['os.arch']} ${System.properties['os.version']}"
'Implementation-Title' : 'sdrtrunk project',
'Implementation-Version' : archiveVersion,
'Class-Path' : 'jmbe-1.0.4.jar',
'Created-By' : "Gradle ${gradle.gradleVersion}",
'Build-Timestamp' : new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").format(new Date()),
'Build-JDK' : "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']}",
'Build-OS' : "${System.properties['os.name']} (${System.properties['os.arch']} ${System.properties['os.version']}"
)
}
}
/**
* Java Development Kit (JDK) locations. These paths must point to the 'bin' directory
* within a JDK for each of the specified architectures. These JDKs are used by the
* runtime and runtimeZip tasks to produce platform-specific builds
* Java Development Kit (JDK) locations. In order to build OS-specific images, these paths must point to the 'bin'
* directory within a JDK for each of the specified architectures. These JDKs are used by the runtime and runtimeZip
* tasks to produce platform-specific builds. If none of these paths exists, then an image will be created for the
* host OS using the installed JDK.
*/
def jdkLinux_x64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-13.0.1'
def jdkLinux_x86 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-13.0.1'
def jdkOsx_x64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-13.0.1.jdk'
def jdkWindows_x64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-13.0.1'
def jdkWindows_x86 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-13.0.1'
//def jdkLinux_aarch64 = '/media/denny/WD250GB/java/bellsoft/linux-aarch64/jdk-11.0.2'
//def jdkLinux_arm32 = '/media/denny/WD250GB/java/bellsoft/linux-arm32/jdk-11.0.2'
//def jfxLinux_arm32 = '/media/denny/WD250GB/java/bellsoft/linux-arm32/armv6hf-sdk/lib'
def jdk_linux_x86_64 = '/media/denny/WD250GB/java/bellsoft/linux-x64/jdk-14'
def jdk_linux_x86_32 = '/media/denny/WD250GB/java/bellsoft/linux-x86/jdk-14'
def jdk_osx_x86_64 = '/media/denny/WD250GB/java/bellsoft/osx-x64/jdk-14.jdk'
def jdk_windows_x86_64 = '/media/denny/WD250GB/java/bellsoft/windows-x64/jdk-14'
def jdk_windows_x86_32 = '/media/denny/WD250GB/java/bellsoft/windows-x86/jdk-14'
def file_jdk_linux_x86_64 = new File(jdk_linux_x86_64)
def file_jdk_linux_x86_32 = new File(jdk_linux_x86_32)
def file_jdk_osx_x86_64 = new File(jdk_osx_x86_64)
def file_jdk_windows_x86_64 = new File(jdk_windows_x86_64)
def file_jdk_windows_x86_32 = new File(jdk_windows_x86_32)
def hasTargetJdk = file_jdk_linux_x86_32.exists() ||
file_jdk_linux_x86_64.exists() ||
file_jdk_osx_x86_64.exists() ||
file_jdk_windows_x86_32.exists() ||
file_jdk_windows_x86_64.exists()
runtime {
targetPlatform('linux-x86_64', jdkLinux_x64)
targetPlatform('linux-x86_32', jdkLinux_x86)
targetPlatform('osx-x86_64', jdkOsx_x64)
targetPlatform('windows-x86_64', jdkWindows_x64)
targetPlatform('windows-x86_32', jdkWindows_x86)
// targetPlatform('linux-aarch64', jdkLinux_aarch64)
// targetPlatform('linux-arm32', jdkLinux_arm32)
if(file_jdk_linux_x86_64.exists())
{
targetPlatform('linux-x86_64-v' + version, jdk_linux_x86_64)
}
if(file_jdk_linux_x86_32.exists())
{
targetPlatform('linux-x86_32-v' + version, jdk_linux_x86_32)
}
if(file_jdk_osx_x86_64.exists())
{
targetPlatform('osx-x86_64-v' + version, jdk_osx_x86_64)
}
if(file_jdk_windows_x86_64.exists())
{
targetPlatform('windows-x86_64-v' + version, jdk_windows_x86_64)
}
if(file_jdk_windows_x86_32.exists())
{
targetPlatform('windows-x86_32-v' + version, jdk_windows_x86_32)
}
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop']
imageZip = file("$buildDir/image/sdr-trunk-" + version + ".zip")
modules = ['java.desktop', 'java.naming', 'jdk.unsupported', 'jdk.unsupported.desktop', 'java.net.http']
imageZip = hasTargetJdk ? file("$buildDir/image/sdr-trunk.zip") : file("$buildDir/image/sdr-trunk-" + version + ".zip")
}
/**
* Check for existence of JDK folders for each supported platform before creating runtime images
*/
tasks.runtime.doFirst {
def linux_x64 = new File(jdkLinux_x64)
if(!linux_x64.exists())
if(!file_jdk_linux_x86_64.exists())
{
println("Linux x64 JDK was not found at " + jdkLinux_x64)
throw new GradleException("Cannot find Java Development Kit (JDK) for linux-x64 architecture. " +
"Please update the build.gradle script to provide the correct path")
println("Skipping OS Image - Linux x86 64-bit JDK was not found at " + jdk_linux_x86_64)
}
def linux_x86 = new File(jdkLinux_x86)
if(!linux_x86.exists())
if(!file_jdk_linux_x86_32.exists())
{
println("Linux x86 JDK was not found at " + jdkLinux_x86)
throw new GradleException("Cannot find Java Development Kit (JDK) for linux-x86 32-bit architecture. " +
"Please update the build.gradle script to provide the correct path")
println("Skipping OS Image - Linux x86 32-bit JDK was not found at " + jdk_linux_x86_32)
}
def osx_x64 = new File(jdkOsx_x64)
if(!osx_x64.exists())
if(!file_jdk_osx_x86_64.exists())
{
println("OSX x64 JDK was not found at " + jdkOsx_x64);
throw new GradleException("Cannot find Java Development Kit (JDK) for osx-x64 architecture. " +
"Please update the build.gradle script to provide the correct path")
println("Skipping OS Image - OSX x86 64-bit JDK was not found at " + jdk_osx_x86_64);
}
def windows_x64 = new File(jdkWindows_x64)
if(!windows_x64.exists())
if(!file_jdk_windows_x86_64.exists())
{
println("Windows x64 JDK was not found at " + jdkWindows_x64)
throw new GradleException("Cannot find Java Development Kit (JDK) for windows-x64 architecture. " +
"Please update the build.gradle script to provide the correct path")
println("Skipping OS Image - Windows x86 64-bit JDK was not found at " + jdk_windows_x86_64)
}
def windows_x86 = new File(jdkWindows_x86)
if(!windows_x86.exists())
if(!file_jdk_windows_x86_32.exists())
{
println("Windows x86 32-bit JDK was not found at " + jdkWindows_x86)
throw new GradleException("Cannot find Java Development Kit (JDK) for windows-x64 architecture. " +
"Please update the build.gradle script to provide the correct path")
println("Skipping OS Image - Windows x86 32-bit JDK was not found at " + jdk_windows_x86_32)
}
// def linux_aarch64 = new File(jdkLinux_aarch64)
//
// if(!linux_aarch64.exists())
// {
// println("Linux aarch64 JDK was not found at " + jdkLinux_aarch64)
// throw new GradleException("Cannot find Java Development Kit (JDK) for linux-aarch64 architecture. " +
// "Please update the build.gradle script to provide the correct path")
// }
//
// def linux_arm32 = new File(jdkLinux_arm32)
//
// if(!linux_arm32.exists())
// {
// println("Linux arm32 JDK was not found at " + jdkLinux_arm32)
// throw new GradleException("Cannot find Java Development Kit (JDK) for linux-arm32 architecture. " +
// "Please update the build.gradle script to provide the correct path")
// }
}
/**
* Since we have to list each of the platform-specific JavaFX libraries as dependencies,
* remove them from each build image
* Remove unused JavaFX OS-specific libraries from each platform target directory
*/
tasks.runtime.doLast {
delete(fileTree('build/image/sdr-trunk-linux-x86_64/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_32/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-osx-x86_64/lib').include { it.name ==~ /javafx.*-(win|linux)\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_64/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_32/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_32-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-osx-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(win|linux)\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_64-v' + version + '/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_32-v' + version + '/lib').include { it.name ==~ /javafx.*-(linux|mac)\.jar/ })
// delete(fileTree('build/image/sdr-trunk-linux-aarch64/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
// delete(fileTree('build/image/sdr-trunk-linux-arm32/lib').include { it.name ==~ /javafx.*-(win|mac)\.jar/ })
// copy {
// from(jfxLinux_arm32).exclude("src.zip")
// into 'build/image/sdr-trunk-linux-arm32/lib'
// }
//JavaFX web and media modules are transitive dependencies of ControlsFX but are not required for this application
delete(fileTree('build/image/sdr-trunk-linux-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-linux-x86_32-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-osx-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_64-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
delete(fileTree('build/image/sdr-trunk-windows-x86_32-v' + version + '/lib').include { it.name ==~ /javafx-(web|media)-.*\.jar/ })
}

16
copyright.template Normal file
View 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/>
*****************************************************************************

Binary file not shown.

View File

@@ -1,27 +1,5 @@
#
# /*
# * ******************************************************************************
# * Copyright (C) 2014-2019 Dennis Sheirer
# *
# * This program is free software: you can redistribute it and/or modify
# * it under the terms of the GNU General Public License as published by
# * the Free Software Foundation, either version 3 of the License, or
# * (at your option) any later version.
# *
# * This program is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# * GNU General Public License for more details.
# *
# * You should have received a copy of the GNU General Public License
# * along with this program. If not, see <http://www.gnu.org/licenses/>
# * *****************************************************************************
# */
#
#
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

51
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -109,8 +125,8 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
@@ -138,19 +154,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +175,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

18
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -30,6 +30,7 @@
<component name="DependencyValidationManager">
<option name="SKIP_IMPORT_STATEMENTS" value="false" />
</component>
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@@ -60,21 +61,7 @@
<version value="1.0" />
</component>
<component name="JavadocGenerationManager">
<option name="OUTPUT_DIRECTORY" />
<option name="OPTION_SCOPE" value="protected" />
<option name="OPTION_HIERARCHY" value="true" />
<option name="OPTION_NAVIGATOR" value="true" />
<option name="OPTION_INDEX" value="true" />
<option name="OPTION_SEPARATE_INDEX" value="true" />
<option name="OPTION_DOCUMENT_TAG_USE" value="false" />
<option name="OPTION_DOCUMENT_TAG_AUTHOR" value="false" />
<option name="OPTION_DOCUMENT_TAG_VERSION" value="false" />
<option name="OPTION_DOCUMENT_TAG_DEPRECATED" value="true" />
<option name="OPTION_DEPRECATED_LIST" value="true" />
<option name="OTHER_OPTIONS" value="" />
<option name="HEAP_SIZE" />
<option name="LOCALE" />
<option name="OPEN_IN_BROWSER" value="true" />
</component>
<component name="Palette2">
<group name="Swing">

View File

@@ -538,9 +538,12 @@ public class AliasList implements Listener<AliasEvent>
{
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
mLog.warn("Alias [" + alias.getName() + "] talkgroup [" + talkgroup.getValue() +
"] has the same talkgroup value as alias [" + existing.getName() +
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
if(!existing.equals(alias))
{
mLog.warn("Alias [" + alias.getName() + "] talkgroup [" + talkgroup.getValue() +
"] has the same talkgroup value as alias [" + existing.getName() +
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
}
}
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
@@ -551,7 +554,7 @@ public class AliasList implements Listener<AliasEvent>
//Log warning if the new talkgroup range overlaps with any existing ranges
for(Map.Entry<TalkgroupRange,Alias> entry: mTalkgroupRangeAliasMap.entrySet())
{
if(talkgroupRange.overlaps(entry.getKey()))
if(talkgroupRange.overlaps(entry.getKey()) && !entry.getValue().equals(alias))
{
mLog.warn("Alias [" + alias.getName() + "] with talkgroup range [" + talkgroupRange.toString() +
"] overlaps with alias [" + entry.getValue().getName() +
@@ -621,9 +624,12 @@ public class AliasList implements Listener<AliasEvent>
{
Alias existing = mRadioAliasMap.get(radio.getValue());
mLog.warn("Alias [" + alias.getName() + "] radio ID [" + radio.getValue() +
"] has the same value as alias [" + existing.getName() +
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
if(!existing.equals(alias))
{
mLog.warn("Alias [" + alias.getName() + "] radio ID [" + radio.getValue() +
"] has the same value as alias [" + existing.getName() +
"] - alias [" + alias.getName() + "] will be used for alias list [" + getName() + "]");
}
}
mRadioAliasMap.put(radio.getValue(), alias);
@@ -634,7 +640,7 @@ public class AliasList implements Listener<AliasEvent>
//Log warning if the new range overlaps with any existing ranges
for(Map.Entry<RadioRange,Alias> entry: mRadioRangeAliasMap.entrySet())
{
if(radioRange.overlaps(entry.getKey()))
if(radioRange.overlaps(entry.getKey()) && !entry.getValue().equals(alias))
{
mLog.warn("Alias [" + alias.getName() + "] with radio ID range [" + radioRange.toString() +
"] overlaps with alias [" + entry.getValue().getName() +

View File

@@ -78,8 +78,8 @@ public class Radio extends AliasID
return false;
}
TalkgroupFormat talkgroupFormat = TalkgroupFormat.get(mProtocol);
return talkgroupFormat.getMinimumValidValue() <= mValue && mValue <= talkgroupFormat.getMaximumValidValue();
RadioFormat radioFormat = RadioFormat.get(mProtocol);
return radioFormat.getMinimumValidValue() <= mValue && mValue <= radioFormat.getMaximumValidValue();
}
public String toString()

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -34,8 +34,8 @@ import io.github.dsheirer.protocol.Protocol;
*/
public enum RadioFormat
{
APCO25("********", 1, 0xFFFFFF, "1 to 16,777,215",
"<html>APCO25 valid range is 1 to 16,777,215"),
APCO25("********", 0, 0xFFFFFF, "0 to 16,777,215",
"<html>APCO25 valid range is 0 to 16,777,215"),
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
"Unknown protocol valid value range is 1-16,777,215");

View File

@@ -1,21 +1,23 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
package io.github.dsheirer.alias.id.talkgroup;
@@ -32,8 +34,8 @@ import io.github.dsheirer.protocol.Protocol;
*/
public enum TalkgroupFormat
{
APCO25("********", 1, 0xFFFFFF, "1 to 16,777,215",
"<html>APCO25 valid range is 1 to 65,535(talkgroup)<br>or 1 to 16,777,215 (radio ID)"),
APCO25("********", 0, 0xFFFFFF, "0 to 65,535",
"<html>APCO25 talkgroup valid range is 0 to 65,535"),
FLEETSYNC("###-####", 1, 0x7FFFFF, "001-0001 to 127-8192",
"<html>Fleetsync valid ranges are 1-127(prefix)<br>and 1-8192(ident) (ie. 001-0001 to 127-8192)"),
LTR("##-###", 257, 5375, "01-001 to 20-255",

View File

@@ -20,41 +20,134 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.identifier.IdentifierUpdateListener;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base audio module implementation.
*/
public abstract class AbstractAudioModule extends Module implements IAudioPacketProvider, IdentifierUpdateListener,
ISquelchStateListener
public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener
{
//Static unique audio channel identifier.
private static int AUDIO_CHANNEL_ID_GENERATOR = 1;
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class);
private static final int MAX_SEGMENT_AUDIO_SAMPLE_LENGTH = 8000 * 60 * 1; //8 kHz - 1 minute
private int mAudioChannelId = AUDIO_CHANNEL_ID_GENERATOR++;
private Listener<ReusableAudioPacket> mAudioPacketListener;
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Listener<AudioSegment> mAudioSegmentListener;
protected MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
private AliasList mAliasList;
private AudioSegment mAudioSegment;
private int mAudioSampleCount = 0;
private boolean mRecordAudioOverride;
/**
* Constructs an abstract audio module
*/
public AbstractAudioModule()
public AbstractAudioModule(AliasList aliasList)
{
mAliasList = aliasList;
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
}
protected abstract int getTimeslot();
/**
* Closes the current audio segment
*/
protected void closeAudioSegment()
{
synchronized(this)
{
if(mAudioSegment != null)
{
mAudioSegment.completeProperty().set(true);
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
mAudioSegment = null;
}
}
}
@Override
public void stop()
{
closeAudioSegment();
}
/**
* Unique channel identifier for use in tagging audio packets with an audio channel ID so that they can
* be identified within a combined audio packet stream
* Gets the current audio segment, or creates a new audio segment as necessary and broadcasts it to any registered
* listener(s).
*/
public int getAudioChannelId()
protected AudioSegment getAudioSegment()
{
return mAudioChannelId;
synchronized(this)
{
if(mAudioSegment == null)
{
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
if(mRecordAudioOverride)
{
mAudioSegment.recordAudioProperty().set(true);
}
if(mAudioSegmentListener != null)
{
mAudioSegment.incrementConsumerCount();
mAudioSegmentListener.receive(mAudioSegment);
}
mAudioSampleCount = 0;
}
return mAudioSegment;
}
}
protected void addAudio(float[] audioBuffer)
{
AudioSegment audioSegment = getAudioSegment();
//If the current segment exceeds the max samples length, close it so that a new segment gets generated
//and then link the segments together
if(mAudioSampleCount >= MAX_SEGMENT_AUDIO_SAMPLE_LENGTH)
{
AudioSegment previous = getAudioSegment();
closeAudioSegment();
audioSegment = getAudioSegment();
audioSegment.linkTo(previous);
}
audioSegment.addAudio(audioBuffer);
mAudioSampleCount += audioBuffer.length;
}
/**
* Sets all audio segments as recordable when the argument is true. Otherwise, defers to the aliased identifiers
* from the identifier collection to determine whether to record the audio or not.
* @param recordAudio set to true to mark all audio as recordable.
*/
public void setRecordAudio(boolean recordAudio)
{
mRecordAudioOverride = recordAudio;
if(mRecordAudioOverride)
{
synchronized(this)
{
if(mAudioSegment != null)
{
mAudioSegment.recordAudioProperty().set(true);
}
}
}
}
/**
@@ -63,7 +156,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioPacket
@Override
public Listener<IdentifierUpdateNotification> getIdentifierUpdateListener()
{
return mIdentifierCollection;
return mIdentifierUpdateNotificationBroadcaster;
}
/**
@@ -75,37 +168,20 @@ public abstract class AbstractAudioModule extends Module implements IAudioPacket
}
/**
* Registers an audio packet listener to receive the output from this audio module.
* Registers an audio segment listener to receive the output from this audio module.
*/
@Override
public void setAudioPacketListener(Listener<ReusableAudioPacket> listener)
public void setAudioSegmentListener(Listener<AudioSegment> listener)
{
mAudioPacketListener = listener;
mAudioSegmentListener = listener;
}
/**
* Unregisters the audio packet listener from receiving audio packets from this module.
* Unregisters the audio segment listener from receiving audio segments from this module.
*/
@Override
public void removeAudioPacketListener()
public void removeAudioSegmentListener()
{
mAudioPacketListener = null;
mAudioSegmentListener = null;
}
/**
* Registered listener for receiving audio packets produced by this module
*/
public Listener<ReusableAudioPacket> getAudioPacketListener()
{
return mAudioPacketListener;
}
/**
* Indicates if there is a listener registered to receive audio packets from this audio module.
*/
public boolean hasAudioPacketListener()
{
return mAudioPacketListener != null;
}
}

View File

@@ -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()));
}
}
}
}
}

View File

@@ -21,6 +21,8 @@
*/
package io.github.dsheirer.audio;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
@@ -29,23 +31,19 @@ import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IReusableBufferListener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Provides packaging of demodulated audio sample buffers into audio packets for broadcast to registered audio packet
* listeners. Includes audio packet metadata in constructed audio packets.
* Provides packaging of demodulated audio sample buffers into audio segments for broadcast to registered listeners.
* Includes audio packet metadata in constructed audio segments.
*
* Incorporates audio squelch state listener to control if audio packets are broadcast or ignored.
*
* This class is designed to support 8 kHz sample rate demodulated audio.
*/
public class AudioModule extends AbstractAudioModule implements IReusableBufferListener, Listener<ReusableFloatBuffer>
public class AudioModule extends AbstractAudioModule implements ISquelchStateListener, IReusableBufferListener,
Listener<ReusableFloatBuffer>
{
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
private static float[] sHighPassFilterCoefficients;
@@ -75,36 +73,31 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
}
}
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("AudioModule");
private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients);
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
private boolean mRecordAudioOverride;
/**
* Creates an Audio Module.
*/
public AudioModule()
public AudioModule(AliasList aliasList)
{
super(aliasList);
}
@Override
protected int getTimeslot()
{
return 0;
}
@Override
public void dispose()
{
removeAudioPacketListener();
removeAudioSegmentListener();
mSquelchStateListener = null;
}
/**
* Sets all audio packets as recordable when the argument is true. Otherwise, defers to the aliased identifiers
* from the identifier collection to determine whether to record the audio or not.
* @param recordAudio set to true to mark all audio as recordable.
*/
public void setRecordAudio(boolean recordAudio)
{
mRecordAudioOverride = recordAudio;
}
@Override
public void reset()
{
@@ -114,30 +107,8 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
@Override
public void start()
{
/* No start operations provided */
}
@Override
public void stop()
{
/* Issue an end-audio packet in case a recorder is still rolling */
if(hasAudioPacketListener())
{
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
endAudioPacket.resetAttributes();
endAudioPacket.setAudioChannelId(getAudioChannelId());
endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf());
if(mRecordAudioOverride)
{
endAudioPacket.setRecordable(true);
}
endAudioPacket.incrementUserCount();
getAudioPacketListener().receive(endAudioPacket);
}
}
@Override
public Listener<SquelchStateEvent> getSquelchStateListener()
{
@@ -147,23 +118,11 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
@Override
public void receive(ReusableFloatBuffer reusableFloatBuffer)
{
if(hasAudioPacketListener() && mSquelchState == SquelchState.UNSQUELCH)
if(mSquelchState == SquelchState.UNSQUELCH)
{
ReusableFloatBuffer highPassFiltered = mHighPassFilter.filter(reusableFloatBuffer);
ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount());
audioPacket.resetAttributes();
audioPacket.setAudioChannelId(getAudioChannelId());
audioPacket.loadAudioFrom(highPassFiltered);
if(mRecordAudioOverride)
{
audioPacket.setRecordable(true);
}
audioPacket.setIdentifierCollection(getIdentifierCollection().copyOf());
getAudioPacketListener().receive(audioPacket);
highPassFiltered.decrementUserCount();
ReusableFloatBuffer buffer = mHighPassFilter.filter(reusableFloatBuffer);
addAudio(buffer.getSamples());
buffer.decrementUserCount();
}
else
{
@@ -186,16 +145,12 @@ public class AudioModule extends AbstractAudioModule implements IReusableBufferL
@Override
public void receive(SquelchStateEvent event)
{
if(event.getSquelchState() == SquelchState.SQUELCH && hasAudioPacketListener())
{
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
endAudioPacket.resetAttributes();
endAudioPacket.setAudioChannelId(getAudioChannelId());
endAudioPacket.setIdentifierCollection(getIdentifierCollection().copyOf());
getAudioPacketListener().receive(endAudioPacket);
}
mSquelchState = event.getSquelchState();
if(mSquelchState == SquelchState.SQUELCH)
{
closeAudioSegment();
}
}
}
}

View File

@@ -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);
}
}

View 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();
}
}

View File

@@ -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();
}
}

View File

@@ -16,7 +16,6 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.ConversionUtils;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,18 +31,17 @@ public class AudioUtils
/**
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
*/
public static byte[] convertTo16BitSamples(List<ReusableAudioPacket> audioPackets)
public static byte[] convertTo16BitSamples(List<float[]> buffers)
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try
{
for(ReusableAudioPacket audioPacket: audioPackets)
for(float[] audioBuffer: buffers)
{
//Converting from 32-bit floats to signed 16-bit samples
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples());
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioBuffer);
stream.write(buffer.array());
audioPacket.decrementUserCount();
}
}
catch(IOException e)
@@ -53,4 +51,29 @@ public class AudioUtils
return stream.toByteArray();
}
/**
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
*/
public static byte[] convert(List<float[]> audioBuffers)
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try
{
for(float[] audioBuffer: audioBuffers)
{
//Converting from 32-bit floats to signed 16-bit samples
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioBuffer);
stream.write(buffer.array());
}
}
catch(IOException e)
{
mLog.error("Error writing converted PCM bytes to output stream");
}
return stream.toByteArray();
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -204,6 +204,10 @@ public abstract class AudioBroadcaster implements Listener<AudioRecording>
mAudioRecordingQueue.offer(recording);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
else
{
recording.removePendingReplay();
}
}
/**

View File

@@ -18,22 +18,25 @@
******************************************************************************/
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.identifier.IdentifierCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Path;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
public class AudioRecording implements Comparable<AudioRecording>
{
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
private final static Logger mLog = LoggerFactory.getLogger(AudioRecording.class);
private Path mPath;
private long mStartTime;
private long mRecordingLength;
private AtomicInteger mPendingReplayCount = new AtomicInteger();
private IdentifierCollection mIdentifierCollection;
private Collection<BroadcastChannel> mBroadcastChannels;
/**
* Audio recording that is ready to be streamed
@@ -43,9 +46,11 @@ public class AudioRecording implements Comparable<AudioRecording>
* @param start time of recording in milliseconds since epoch
* @param recordingLength in milliseconds
*/
public AudioRecording(Path path, IdentifierCollection identifierCollection, long start, long recordingLength)
public AudioRecording(Path path, Collection<BroadcastChannel> broadcastChannels,
IdentifierCollection identifierCollection, long start, long recordingLength)
{
mPath = path;
mBroadcastChannels = broadcastChannels;
mIdentifierCollection = identifierCollection;
mStartTime = start;
mRecordingLength = recordingLength;
@@ -59,6 +64,14 @@ public class AudioRecording implements Comparable<AudioRecording>
return mPath;
}
/**
* Collection of broadcast channels that this recording should be streamed to
*/
public Collection<BroadcastChannel> getBroadcastChannels()
{
return mBroadcastChannels;
}
/**
* Optional audio metadata/identifiers for the recording.
*/

View File

@@ -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);
}
}
}
}

View File

@@ -44,13 +44,9 @@ import io.github.dsheirer.gui.editor.Editor;
import io.github.dsheirer.gui.editor.EmptyEditor;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.AudioRecorder;
import io.github.dsheirer.record.mp3.MP3Recorder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.file.Path;
public class BroadcastFactory
{
private final static Logger mLog = LoggerFactory.getLogger(BroadcastFactory.class);
@@ -186,21 +182,6 @@ public class BroadcastFactory
return editor;
}
/**
* Creates an audio recorder for the specified broadcastAudio format using the specified path output file name
*/
public static AudioRecorder getAudioRecorder(Path path, BroadcastFormat broadcastFormat)
{
switch(broadcastFormat)
{
case MP3:
return new MP3Recorder(path);
default:
mLog.debug("Unrecognized broadcastAudio format [" + broadcastFormat + "] cannot create audio recorder");
return null;
}
}
public static ISilenceGenerator getSilenceGenerator(BroadcastFormat format)
{
switch(format)

View File

@@ -21,7 +21,6 @@
*/
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.icon.IconManager;
@@ -29,7 +28,6 @@ import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,7 +49,7 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BroadcastModel extends AbstractTableModel implements Listener<ReusableAudioPacket>
public class BroadcastModel extends AbstractTableModel implements Listener<AudioRecording>
{
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
@@ -76,7 +74,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
private Map<String,BroadcastConfiguration> mBroadcastConfigurationMap = new HashMap<>();
private Map<String,AudioBroadcaster> mBroadcasterMap = new HashMap<>();
private IconManager mIconManager;
private StreamManager mStreamManager;
private AliasModel mAliasModel;
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
@@ -87,8 +84,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
{
mAliasModel = aliasModel;
mIconManager = iconManager;
mStreamManager = new StreamManager(new CompletedRecordingListener(), BroadcastFormat.MP3, userPreferences);
mStreamManager.start();
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS);
@@ -281,22 +276,28 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
}
@Override
public void receive(ReusableAudioPacket audioPacket)
public void receive(AudioRecording audioRecording)
{
if(audioPacket.isStreamable())
if(audioRecording != null && !audioRecording.getBroadcastChannels().isEmpty())
{
for(BroadcastChannel channel: audioPacket.getBroadcastChannels())
for(BroadcastChannel broadcastChannel : audioRecording.getBroadcastChannels())
{
if(mBroadcasterMap.containsKey(channel.getChannelName()))
String channelName = broadcastChannel.getChannelName();
if(channelName != null)
{
audioPacket.incrementUserCount();
mStreamManager.receive(audioPacket);
return;
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
if(audioBroadcaster != null)
{
audioRecording.addPendingReplay();
audioBroadcaster.receive(audioRecording);
}
}
}
}
audioPacket.decrementUserCount();
mRecordingQueue.add(audioRecording);
}
/**
@@ -311,19 +312,9 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
if(audioBroadcaster != null)
{
audioBroadcaster.setListener(new Listener<BroadcastEvent>()
{
@Override
public void receive(BroadcastEvent broadcastEvent)
{
process(broadcastEvent);
}
});
audioBroadcaster.setListener(broadcastEvent -> process(broadcastEvent));
audioBroadcaster.start();
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getName(), audioBroadcaster);
int index = mBroadcastConfigurations.indexOf(audioBroadcaster.getBroadcastConfiguration());
if(index >= 0)
@@ -710,43 +701,6 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Reusa
}
}
/**
* Processes completed audio recordings and distributes them to the audio broadcasters. Adds the recording to
* the audio recording queue to be monitored for deletion.
*/
public class CompletedRecordingListener implements Listener<AudioRecording>
{
@Override
public void receive(AudioRecording audioRecording)
{
if(audioRecording != null && audioRecording.hasIdentifierCollection())
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
if(aliasList != null)
{
for(BroadcastChannel broadcastChannel : aliasList.getBroadcastChannels(audioRecording.getIdentifierCollection()))
{
String channelName = broadcastChannel.getChannelName();
if(channelName != null)
{
AudioBroadcaster audioBroadcaster = getBroadcaster(channelName);
if(audioBroadcaster != null)
{
audioRecording.addPendingReplay();
audioBroadcaster.receive(audioRecording);
}
}
}
}
}
mRecordingQueue.add(audioRecording);
}
}
/**
* Monitors the recording queue and removes any recordings that have no pending replays by audio broadcasters
*/

View File

@@ -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());
});
}
}
}
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -178,6 +178,15 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
}
if(to == null)
{
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
if(!toIdentifiers.isEmpty())
{
to = toIdentifiers.get(0);
}
}
if(to != null)
{
sb.append("TO:").append(to);
@@ -194,7 +203,17 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
sb.append("TO:UNKNOWN");
}
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.FROM);
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
if(from == null)
{
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
if(!fromIdentifiers.isEmpty())
{
from = fromIdentifiers.get(0);
}
}
if(from != null)
{

View File

@@ -1,21 +1,23 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
@@ -203,6 +205,15 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
}
if(to == null)
{
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
if(!toIdentifiers.isEmpty())
{
to = toIdentifiers.get(0);
}
}
if(to != null)
{
sb.append("TO:").append(to);
@@ -219,7 +230,17 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
sb.append("TO:UNKNOWN");
}
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.FROM);
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
if(from == null)
{
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
if(!fromIdentifiers.isEmpty())
{
from = fromIdentifiers.get(0);
}
}
if(from != null)
{

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -291,6 +291,16 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
}
if(to == null)
{
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
if(!toIdentifiers.isEmpty())
{
to = toIdentifiers.get(0);
}
}
if(to != null)
{
sbTitle2.append("TO:").append(to);
@@ -307,7 +317,17 @@ public class ShoutcastV2AudioBroadcaster extends AudioBroadcaster implements IBr
sbTitle2.append("TO:UNKNOWN");
}
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.FROM);
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
if(from == null)
{
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
if(!fromIdentifiers.isEmpty())
{
from = fromIdentifiers.get(0);
}
}
if(from != null)
{

View File

@@ -22,6 +22,7 @@
package io.github.dsheirer.audio.codec.mbe;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.preference.UserPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,9 +33,9 @@ public abstract class AmbeAudioModule extends JmbeAudioModule
private static final String AMBE_CODEC = "AMBE 3600 x 2450";
private static boolean sLibraryStatusLogged = false;
public AmbeAudioModule(UserPreferences userPreferences)
public AmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
{
super(userPreferences);
super(userPreferences, aliasList);
if(!sLibraryStatusLogged)
{

View File

@@ -22,6 +22,7 @@
package io.github.dsheirer.audio.codec.mbe;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.preference.UserPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -32,9 +33,9 @@ public abstract class ImbeAudioModule extends JmbeAudioModule
private static final String IMBE_CODEC = "IMBE";
private static boolean sLibraryStatusLogged = false;
public ImbeAudioModule(UserPreferences userPreferences)
public ImbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
{
super(userPreferences);
super(userPreferences, aliasList);
if(!sLibraryStatusLogged)
{

View File

@@ -23,14 +23,15 @@
package io.github.dsheirer.audio.codec.mbe;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.AbstractAudioModule;
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import jmbe.iface.IAudioCodec;
import jmbe.iface.IAudioCodecLibrary;
import org.slf4j.Logger;
@@ -44,17 +45,18 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
ISquelchStateListener
{
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
private static List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private IAudioCodec mAudioCodec;
private UserPreferences mUserPreferences;
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("JmbeAudioModule");
public JmbeAudioModule(UserPreferences userPreferences)
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList)
{
super(aliasList);
mUserPreferences = userPreferences;
MyEventBus.getEventBus().register(this);
loadConverter();
@@ -73,11 +75,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
return getAudioCodec() != null;
}
protected ReusableAudioPacketQueue getAudioPacketQueue()
{
return mAudioPacketQueue;
}
@Override
public Listener<IMessage> getMessageListener()
{

View File

@@ -22,21 +22,9 @@
package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.audio.convert.thumbdv.ThumbDv;
import io.github.dsheirer.audio.convert.thumbdv.message.response.AmbeResponse;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import io.github.dsheirer.record.wave.AudioPacketWaveRecorder;
import io.github.dsheirer.record.wave.WaveMetadata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Utility for converting MBE call sequences (*.mbe) to PCM wave audio format
*/
@@ -44,64 +32,64 @@ public class MBECallSequenceConverter
{
private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceConverter.class);
public static void convert(Path input, Path output) throws IOException
{
InputStream inputStream = Files.newInputStream(input);
ObjectMapper mapper = new ObjectMapper();
MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
convert(sequence, output);
}
public static void convert(MBECallSequence callSequence, Path outputPath)
{
if(callSequence == null || callSequence.isEncrypted())
{
throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
}
if(callSequence != null && !callSequence.isEncrypted())
{
ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
recorder.start();
long delayMillis = 0;
try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
{
thumbDv.start();
for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
{
mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
thumbDv.decode(voiceFrame.getFrameBytes());
delayMillis += 30;
}
if(delayMillis > 0)
{
delayMillis += 1000;
try
{
Thread.sleep(delayMillis);
}
catch(InterruptedException ie)
{
}
}
}
catch(IOException ioe)
{
mLog.error("Error", ioe);
}
recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
}
}
public static void main(String[] args)
{
// public static void convert(Path input, Path output) throws IOException
// {
// InputStream inputStream = Files.newInputStream(input);
// ObjectMapper mapper = new ObjectMapper();
// MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
// convert(sequence, output);
// }
//
// public static void convert(MBECallSequence callSequence, Path outputPath)
// {
// if(callSequence == null || callSequence.isEncrypted())
// {
// throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
// }
//
// if(callSequence != null && !callSequence.isEncrypted())
// {
// ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
//
// AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
// recorder.start();
//
// long delayMillis = 0;
//
// try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
// {
// thumbDv.start();
// for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
// {
// mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
// thumbDv.decode(voiceFrame.getFrameBytes());
// delayMillis += 30;
// }
//
// if(delayMillis > 0)
// {
// delayMillis += 1000;
// try
// {
// Thread.sleep(delayMillis);
// }
// catch(InterruptedException ie)
// {
//
// }
// }
// }
// catch(IOException ioe)
// {
// mLog.error("Error", ioe);
// }
//
// recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
// }
// }
//
// public static void main(String[] args)
// {
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_3_TS0_65084_6591007.mbe";
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe";
//
@@ -118,5 +106,5 @@ public class MBECallSequenceConverter
// {
// mLog.error("Error", ioe);
// }
}
// }
}

View File

@@ -18,8 +18,6 @@
******************************************************************************/
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import java.util.List;
public interface IAudioConverter
@@ -27,7 +25,7 @@ public interface IAudioConverter
/**
* Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
*/
public byte[] convert(List<ReusableAudioPacket> audioPackets);
public byte[] convert(List<float[]> audioBuffers);
/**
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the

View File

@@ -20,7 +20,6 @@ package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.audio.AudioUtils;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import net.sourceforge.lame.lowlevel.LameEncoder;
import net.sourceforge.lame.mp3.Lame;
import net.sourceforge.lame.mp3.MPEGMode;
@@ -54,7 +53,7 @@ public class MP3AudioConverter implements IAudioConverter
}
@Override
public byte[] convert(List<ReusableAudioPacket> audioPackets)
public byte[] convert(List<float[]> audioPackets)
{
mMP3Stream.reset();
@@ -84,6 +83,36 @@ public class MP3AudioConverter implements IAudioConverter
}
}
public byte[] convertAudio(List<float[]> audioBuffers)
{
mMP3Stream.reset();
byte[] pcmBytes = AudioUtils.convert(audioBuffers);
int pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length);
int mp3BufferSize = 0;
int pcmBytesPosition = 0;
try
{
while (0 < (mp3BufferSize = mEncoder.encodeBuffer(pcmBytes, pcmBytesPosition, pcmBufferSize, mMP3Buffer)))
{
pcmBytesPosition += pcmBufferSize;
pcmBufferSize = Math.min(mMP3Buffer.length, pcmBytes.length - pcmBytesPosition);
mMP3Stream.write(mMP3Buffer, 0, mp3BufferSize);
}
return mMP3Stream.toByteArray();
}
catch(Exception e)
{
mLog.error("There was an error converting audio to MP3: " + e.getMessage());
return new byte[0];
}
}
@Override
public byte[] flush()
{

View File

@@ -18,9 +18,6 @@
******************************************************************************/
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.record.mp3.MP3Recorder;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,10 +28,11 @@ import java.util.List;
public class MP3SilenceGenerator implements ISilenceGenerator
{
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
public static final int MP3_BIT_RATE = 16;
public static final boolean CONSTANT_BIT_RATE = false;
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3_BIT_RATE, CONSTANT_BIT_RATE);
private byte[] mPreviousPartialFrameData;
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("MP3 Silence Generator");
/**
* Generates MP3 audio silence frames
@@ -46,13 +44,10 @@ public class MP3SilenceGenerator implements ISilenceGenerator
public byte[] generate(long duration)
{
int length = (int)(duration * 8); //8000 Hz sample rate
ReusableAudioPacket silencePacket = mAudioPacketQueue.getBuffer(length);
Arrays.fill(silencePacket.getAudioSamples(), 0.0f);
List<ReusableAudioPacket> silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
byte[] frameData = mGenerator.convert(silencePackets);
List<float[]> silenceBuffers = new ArrayList<>();
silenceBuffers.add(new float[length]);
byte[] frameData = mGenerator.convert(silenceBuffers);
frameData = merge(mPreviousPartialFrameData, frameData);

View File

@@ -36,8 +36,8 @@ import io.github.dsheirer.audio.convert.thumbdv.message.response.DecodeSpeechRes
import io.github.dsheirer.audio.convert.thumbdv.message.response.ReadyResponse;
import io.github.dsheirer.audio.convert.thumbdv.message.response.SetVocoderParameterResponse;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -82,14 +82,14 @@ public class ThumbDv implements AutoCloseable
private ScheduledFuture mAudioDecodeProcessorHandle;
private LinkedTransferQueue<DecodeSpeechRequest> mDecodeSpeechRequests = new LinkedTransferQueue<>();
private AudioProtocol mAudioProtocol;
private Listener<ReusableAudioPacket> mAudioPacketListener;
private ReusableAudioPacketQueue mReusableAudioPacketQueue = new ReusableAudioPacketQueue("ThumbDv");
private Listener<ReusableFloatBuffer> mAudioBufferListener;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("ThumbDv");
private boolean mStarted;
public ThumbDv(AudioProtocol audioProtocol, Listener<ReusableAudioPacket> listener)
public ThumbDv(AudioProtocol audioProtocol, Listener<ReusableFloatBuffer> listener)
{
mAudioProtocol = audioProtocol;
mAudioPacketListener = listener;
mAudioBufferListener = listener;
}
/**
@@ -206,12 +206,11 @@ public class ThumbDv implements AutoCloseable
{
AmbeMessage message = AmbeMessageFactory.getMessage(bytes);
if(message instanceof DecodeSpeechResponse && mAudioPacketListener != null)
if(message instanceof DecodeSpeechResponse && mAudioBufferListener != null)
{
float[] samples = ((DecodeSpeechResponse)message).getSamples();
ReusableAudioPacket audioPacket = mReusableAudioPacketQueue.getBuffer(samples.length);
audioPacket.loadAudioFrom(samples);
mAudioPacketListener.receive(audioPacket);
ReusableFloatBuffer audioBuffer = mReusableBufferQueue.getBuffer(samples, System.currentTimeMillis());
mAudioBufferListener.receive(audioBuffer);
}
else if(message instanceof ReadyResponse && mAudioDecodeProcessorHandle == null)
{
@@ -457,7 +456,7 @@ public class ThumbDv implements AutoCloseable
mLog.debug("Starting thumb dv thread(s)");
final Listener<ReusableAudioPacket> listener = reusableAudioPacket -> {
final Listener<ReusableFloatBuffer> listener = reusableAudioPacket -> {
mLog.info("Got an audio packet!");
reusableAudioPacket.decrementUserCount();
};

View File

@@ -52,6 +52,8 @@ import java.awt.EventQueue;
import java.awt.Font;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
{
@@ -84,6 +86,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
private Identifier mIdentifier;
private List<Alias> mAliases = Collections.EMPTY_LIST;
private AliasModel mAliasModel;
private Lock mLock = new ReentrantLock();
public AudioChannelPanel(IconManager iconManager, UserPreferences userPreferences, SettingsManager settingsManager,
AudioOutput audioOutput, AliasModel aliasModel)
@@ -127,6 +130,12 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
{
//Deregister from receiving preference update notifications
MyEventBus.getEventBus().unregister(this);
if(mAudioOutput != null)
{
mAudioOutput.removeAudioEventListener(this);
mAudioOutput.removeAudioMetadataListener();
}
}
private void init()
@@ -193,13 +202,24 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
*/
private void resetLabels()
{
boolean updated = mIdentifier != null;
mIdentifier = null;
mAliases = Collections.EMPTY_LIST;
//Protect access to mIdentifier and mAliases
mLock.lock();
if(updated)
try
{
updateLabels();
boolean updated = mIdentifier != null;
mIdentifier = null;
mAliases = Collections.EMPTY_LIST;
//Hold the lock through the label update
if(updated)
{
updateLabels();
}
}
finally
{
mLock.unlock();
}
}
@@ -221,11 +241,28 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
boolean updated = false;
if(toIds.size() == 1)
{
Identifier currentIdentifier = mIdentifier;
//Protect access to mIdentifier and mAliases
mLock.lock();
if(currentIdentifier == null || currentIdentifier != toIds.get(0))
try
{
if(toIds.size() == 1)
{
Identifier currentIdentifier = mIdentifier;
if(currentIdentifier == null || currentIdentifier != toIds.get(0))
{
mIdentifier = toIds.get(0);
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
if(aliasList != null)
{
mAliases = aliasList.getAliases(mIdentifier);
}
updated = true;
}
}
else
{
mIdentifier = toIds.get(0);
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
@@ -236,69 +273,64 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
}
updated = true;
}
}
else
{
mIdentifier = toIds.get(0);
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
if(aliasList != null)
//Hold the lock through the label update
if(updated)
{
mAliases = aliasList.getAliases(mIdentifier);
updateLabels();
}
updated = true;
}
if(updated)
finally
{
updateLabels();
mLock.unlock();
}
}
/**
* Updates the alias label with text and icon from the alias. Note: this
* does not occur on the Swing event thread -- wrap any calls to this
* method with an event thread call.
* Updates the alias label with text and icon from the alias.
*/
private void updateLabels()
{
String identifier = null;
String iconName = null;
if(mAliases.size() == 1)
{
identifier = mAliases.get(0).getName();
iconName = mAliases.get(0).getIconName();
}
else if(mAliases.size() > 1)
{
identifier = Joiner.on(", ").skipNulls().join(mAliases);
}
//Protect access to mIdentifier and mAliases
mLock.lock();
if(identifier == null && mIdentifier != null)
try
{
identifier = mTalkgroupFormatPreference.format(mIdentifier);
}
if(identifier == null)
{
identifier = "-----";
}
final ImageIcon icon = iconName != null ? mIconManager.getIcon(iconName, 18) : null;
final String identifierText = identifier;
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
if(mAliases.size() == 1)
{
identifier = mAliases.get(0).getName();
iconName = mAliases.get(0).getIconName();
}
else if(mAliases.size() > 1)
{
identifier = Joiner.on(", ").skipNulls().join(mAliases);
}
if(identifier == null && mIdentifier != null)
{
identifier = mTalkgroupFormatPreference.format(mIdentifier);
}
if(identifier == null)
{
identifier = "-----";
}
final ImageIcon icon = iconName != null ? mIconManager.getIcon(iconName, 18) : null;
final String identifierText = identifier;
EventQueue.invokeLater(() -> {
mIdentifierLabel.setText(identifierText);
mIconLabel.setIcon(icon);
}
});
});
}
finally
{
mLock.unlock();
}
}
/**
@@ -323,14 +355,16 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
switch(colorSetting.getColorSettingName())
{
case CHANNEL_STATE_LABEL_DECODER:
if(mIdentifierLabel != null)
{
mIdentifierLabel.setForeground(mLabelColor);
}
if(mIconLabel != null)
{
mIconLabel.setForeground(mLabelColor);
}
EventQueue.invokeLater(() -> {
if(mIdentifierLabel != null)
{
mIdentifierLabel.setForeground(mLabelColor);
}
if(mIconLabel != null)
{
mIconLabel.setForeground(mLabelColor);
}
});
break;
default:
break;

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -21,13 +21,23 @@
*/
package io.github.dsheirer.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.util.ThreadPool;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,42 +52,41 @@ import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
public abstract class AudioOutput implements Listener<ReusableAudioPacket>, LineListener
/**
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
* and broadcasts audio segment metadata to registered listeners (ie gui components).
*/
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
{
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
private LinkedTransferQueue<ReusableAudioPacket> mBuffer = new LinkedTransferQueue<>();
private int mBufferStartThreshold;
private int mBufferStopThreshold;
private static IdentifierCollection EMPTY_IDENTIFIER_COLLECTION = new IdentifierCollection(0);
static
{
EMPTY_IDENTIFIER_COLLECTION.setUpdated(true);
}
private Listener<IdentifierCollection> mIdentifierCollectionListener;
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
private ScheduledFuture<?> mProcessorTask;
private ScheduledFuture<?> mProcessorFuture;
private SourceDataLine mOutput;
private Mixer mMixer;
private MixerChannel mMixerChannel;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private boolean mCanProcessAudio = false;
private long mLastActivity = System.currentTimeMillis();
private AudioSegment mCurrentAudioSegment;
private AudioSegment mNextAudioSegment;
private ReentrantLock mLock = new ReentrantLock();
private int mCurrentBufferIndex = 0;
private UserPreferences mUserPreferences;
private BooleanProperty mEmptyProperty = new SimpleBooleanProperty(true);
private IntegerProperty mAudioPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
private ByteBuffer mAudioSegmentStartTone;
private ByteBuffer mAudioSegmentPreemptTone;
private ByteBuffer mAudioSegmentDropTone;
private boolean mRunning = false;
/**
* Single audio channel playback with automatic starting and stopping of the
@@ -94,10 +103,11 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
* @param requestedBufferSize of approximately 1 second of audio
*/
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
int requestedBufferSize)
int requestedBufferSize, UserPreferences userPreferences)
{
mMixer = mixer;
mMixerChannel = mixerChannel;
mUserPreferences = userPreferences;
try
{
@@ -139,46 +149,395 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
}
/* Run the queue processor task every 40 milliseconds or 25 times a second */
mProcessorTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BufferProcessor(),
0, 40, TimeUnit.MILLISECONDS);
//Run the queue processor task every 100 milliseconds or 10 times a second
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
}
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED,
getChannelName());
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED,
getChannelName());
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
mCanProcessAudio = true;
}
}
catch(LineUnavailableException e)
{
mLog.error("Couldn't obtain audio source data line for "
+ "audio output - mixer [" + mMixer.getMixerInfo().getName() + "]");
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
mMixer.getMixerInfo().getName() + "]");
}
updateToneInsertionAudioClips();
//Register to receive directory preference update notifications so we can update the preference items
MyEventBus.getEventBus().register(this);
}
/**
* Boolean property that indicates if this audio output has an audio segment queued or in process of playback.
*/
public BooleanProperty emptyProperty()
{
return mEmptyProperty;
}
/**
* Audio playback priority of the current audio segment or default audio priority if nothing is currently loaded.
*/
public IntegerProperty audioPriorityProperty()
{
return mAudioPriority;
}
/**
* Schedules the audio segment for playback.
*
* Note: if a segment is currently playing and another audio segment is already queued for playback, invoking
* this method will overwrite the queued segment with the argument.
*
* @param audioSegment to schedule for playback.
*/
public void play(AudioSegment audioSegment)
{
if(audioSegment != null)
{
mLock.lock();
if(mNextAudioSegment != null)
{
mNextAudioSegment.decrementConsumerCount();
mNextAudioSegment = null;
}
try
{
mNextAudioSegment = audioSegment;
mEmptyProperty.set(false);
}
finally
{
mLock.unlock();
}
}
}
public void reset()
/**
* Indicates if the audio segment is linked to the current playback audio segment
*/
public boolean isLinkedTo(AudioSegment audioSegment)
{
broadcast(new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName()));
return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment);
}
/**
* Receive and process audio identifier update notifications.
*/
@Override
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
{
if(mCurrentAudioSegment != null)
{
IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection();
if(identifierCollection != null)
{
broadcast(identifierCollection);
}
}
}
/**
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.PLAYBACK)
{
updateToneInsertionAudioClips();
}
}
/**
* Updates audio segment start, drop and preempt tone insertion clips
*/
private void updateToneInsertionAudioClips()
{
mAudioSegmentStartTone = null;
mAudioSegmentPreemptTone = null;
mAudioSegmentDropTone = null;
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
if(start != null)
{
mAudioSegmentStartTone = convert(start);
}
float[] preempt = mUserPreferences.getPlaybackPreference().getPreemptTone();
if(preempt != null)
{
mAudioSegmentPreemptTone = convert(preempt);
}
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
if(drop != null)
{
mAudioSegmentDropTone = convert(drop);
}
}
/**
* Disposes of a processed audio segment and notifies the originator that processing of the segment is complete
* by decrementing the consumer count.
* @param audioSegment to dispose
*/
private void dispose(AudioSegment audioSegment)
{
mAudioPriority.unbind();
if(audioSegment != null)
{
audioSegment.decrementConsumerCount();
audioSegment.removeIdentifierUpdateNotificationListener(this);
}
}
/**
* Generates a tone indicating that a new audio segment is starting
*/
private ByteBuffer getAudioSegmentStartTone()
{
return mAudioSegmentStartTone;
}
/**
* Generates a tone indicating that the current audio segment playback has been preempted for a higher priority
* audio segment that is now starting.
*/
private ByteBuffer getAudioSegmentPreemptionTone()
{
return mAudioSegmentPreemptTone;
}
/**
* Generates a tone indicating that the current audio segment playback has been dropped because the audio segment
* has been flagged as Do Not Monitor after playback has started.
*/
private ByteBuffer getAudioSegmentDropTone()
{
return mAudioSegmentDropTone;
}
/**
* Writes the audio buffer data to the source data line. If the data line is not currently playing, write as
* much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual
* buffer content to the data line as a blocking call.
*
* @param buffer of audio to playback
*/
private void playAudio(ByteBuffer buffer)
{
if(buffer != null)
{
int wrote = 0;
//If the output data line is not running, we can only write up to the available capacity. So, only write
//what will fit initially, start playback, and then use a blocking write for the remainder.
if(!mOutput.isRunning())
{
int toWrite = mOutput.available();
if(toWrite > buffer.array().length)
{
toWrite = buffer.array().length;
}
//Top off the buffer and check if we can start it
wrote += mOutput.write(buffer.array(), 0, toWrite);
checkStart();
}
if(mOutput.isRunning() && wrote < buffer.array().length)
{
//This will block until the buffer is fully written to the data line
mOutput.write(buffer.array(), wrote, buffer.array().length - wrote);
}
}
}
/**
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
* by a threaded processor repeatedly to playback the current audio segment and check for and start a newly
* assigned audio segment. It also handles starting and stopping the playback source data line to avoid audio
* discontinuities due to buffer underruns.
*/
private void processAudio()
{
if(mNextAudioSegment != null)
{
mLock.lock();
try
{
//Remove an assigned audio segment that's subsequently flagged as do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor())
{
mNextAudioSegment = null;
if(mCurrentAudioSegment == null)
{
mEmptyProperty.set(true);
}
}
//For linked audio segments, allow the linked segment to complete first before assigning the next
else if(mNextAudioSegment.isLinked())
{
if(mCurrentAudioSegment == null)
{
mCurrentAudioSegment = mNextAudioSegment;
mNextAudioSegment = null;
mCurrentBufferIndex = 0;
if(mCurrentAudioSegment != null)
{
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
else
{
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
}
}
}
else
{
//Insert audio segment start or audio priority preemption bonk tone
if(mCurrentAudioSegment == null)
{
playAudio(getAudioSegmentStartTone());
}
else if(mCurrentBufferIndex > 0 &&
(!mCurrentAudioSegment.completeProperty().get() ||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount()))
{
playAudio(getAudioSegmentPreemptionTone());
}
else
{
playAudio(getAudioSegmentStartTone());
}
//Close current audio segment
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = mNextAudioSegment;
mNextAudioSegment = null;
mCurrentBufferIndex = 0;
if(mCurrentAudioSegment != null)
{
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
else
{
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
}
}
}
finally
{
mLock.unlock();
}
}
if(mCurrentAudioSegment != null)
{
//Check for completed audio segment or a segment flagged as Do Not Monitor
if(mCurrentAudioSegment.isDoNotMonitor() || (mCurrentAudioSegment.completeProperty().get() &&
mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
{
if(mCurrentAudioSegment.isDoNotMonitor())
{
playAudio(getAudioSegmentDropTone());
}
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = null;
mLock.lock();
try
{
if(mNextAudioSegment == null)
{
mEmptyProperty.set(true);
}
}
finally
{
mLock.unlock();
}
return;
}
//Process any new buffers that have been added to the audio segment. If a next audio segment gets assigned
//while processing, exit the loop so that we can evaluate the next for higher priority preempt. If the next
//segment is a linked segment, ignore it so that we can close out the current segment.
while((mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
!mCurrentAudioSegment.isDoNotMonitor())
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
if(audioBuffer != null)
{
ByteBuffer audio = convert(audioBuffer);
playAudio(audio);
}
}
}
checkStop();
}
/**
* Prepares this audio output for disposal.
*/
public void dispose()
{
mCanProcessAudio = false;
if(mProcessorTask != null)
if(mProcessorFuture != null)
{
mProcessorTask.cancel(true);
mProcessorFuture.cancel(true);
}
mProcessorTask = null;
mProcessorFuture = null;
mBuffer.clear();
mLock.lock();
mAudioEventBroadcaster.dispose();
mAudioEventBroadcaster = null;
try
{
if(mNextAudioSegment != null)
{
mNextAudioSegment.decrementConsumerCount();
mNextAudioSegment = null;
}
}
finally
{
mLock.unlock();
}
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = null;
mAudioEventBroadcaster.clear();
mIdentifierCollectionListener = null;
if(mOutput != null)
@@ -192,10 +551,9 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
}
/**
* Converts the audio packet data into a byte buffer format appropriate for
* the underlying source data line.
* Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line.
*/
protected abstract ByteBuffer convert(ReusableAudioPacket packet);
protected abstract ByteBuffer convert(float[] buffer);
/**
* Audio output channel name
@@ -229,7 +587,7 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
/**
* Broadcasts an audio event to the registered listener
*/
private void broadcast(AudioEvent audioEvent)
private void broadcastAudioEvent(AudioEvent audioEvent)
{
mAudioEventBroadcaster.broadcast(audioEvent);
}
@@ -243,6 +601,9 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
mIdentifierCollectionListener = listener;
}
/**
* Unregisters the current audio metadata listener
*/
public void removeAudioMetadataListener()
{
mIdentifierCollectionListener = null;
@@ -260,138 +621,46 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
}
/**
* Timestamp of either last buffer received or last buffer processed
* Starts audio playback once audio buffer is almost full and remaining capacity falls below the start threshold.
*
* Note: this method should only be invoked from the processAudio() method
*/
public long getLastActivityTimestamp()
private void checkStart()
{
return mLastActivity;
if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold)
{
mOutput.start();
mRunning = true;
}
}
/**
* Updates the last activity timestamp to current system time
* Stops audio playback and drains the audio buffer to empty when the audio buffer is mostly empty and the
* available buffer capacity exceeds the stop threshold
*
* Note: this method should only be invoked from the processAudio() method
*/
public void updateTimestamp()
private void checkStop()
{
mLastActivity = System.currentTimeMillis();
}
@Override
public void receive(ReusableAudioPacket packet)
{
if(mCanProcessAudio)
if(mRunning)
{
//Update the activity timestamp so that this audio output doesn't
//get disconnected before it starts processing the audio stream
updateTimestamp();
mBuffer.add(packet);
}
else
{
packet.decrementUserCount();
}
}
public class BufferProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private List<ReusableAudioPacket> mAudioPackets = new ArrayList<ReusableAudioPacket>();
public BufferProcessor()
{
}
@Override
public void run()
{
try
{
/* The processing flag ensures that only one instance of the
* processor can run at any given time */
if(mProcessing.compareAndSet(false, true))
{
mBuffer.drainTo(mAudioPackets);
for(ReusableAudioPacket packet : mAudioPackets)
{
if(packet.getType() == ReusableAudioPacket.Type.AUDIO)
{
broadcast(packet.getIdentifierCollection());
ByteBuffer buffer = convert(packet);
int wrote = 0;
if(!mOutput.isRunning())
{
int toWrite = mOutput.available();
if(toWrite > buffer.array().length)
{
toWrite = buffer.array().length;
}
//Top off the buffer and check if we can start it
wrote += mOutput.write(buffer.array(), 0, toWrite);
checkStart();
}
if(mOutput.isRunning() && wrote < buffer.array().length)
{
//Blocking write
wrote += mOutput.write(buffer.array(), wrote,
buffer.array().length - wrote);
}
updateTimestamp();
}
else
{
packet.decrementUserCount();
}
}
mAudioPackets.clear();
checkStop();
mProcessing.set(false);
}
}
catch(Exception e)
{
mLog.error("Error while processing audio buffers", e);
}
}
/**
* Starts audio playback once audio buffer is almost full and remaining
* capacity falls below the start threshold.
*/
private void checkStart()
{
if(mCanProcessAudio &&
!mOutput.isRunning() &&
mOutput.available() <= mBufferStartThreshold)
{
mOutput.start();
}
}
/**
* Stops audio playback and drains the audio buffer to empty when the
* audio buffer is mostly empty and the available buffer capacity
* exceeds the stop threshold
*/
private void checkStop()
{
if(mCanProcessAudio && mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
//If the output buffer falls below the threshold then drain the output and stop playback
if(mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
{
mOutput.drain();
mOutput.stop();
broadcast(EMPTY_IDENTIFIER_COLLECTION);
mRunning = false;
}
//If output playback stopped on its own because the buffer emptied, then cleanup
else if(!mOutput.isRunning())
{
mRunning = false;
}
//If we stopped audio playback, broadcast a null identifier to clear the gui panel
if(!mRunning)
{
broadcast(null);
}
}
}
@@ -404,9 +673,7 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
if(mMuteControl != null)
{
mMuteControl.setValue(muted);
broadcast(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED :
AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
}
}
@@ -431,14 +698,17 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
return mGainControl;
}
/**
* Indicates if this audio output has a gain control available
*/
public boolean hasGainControl()
{
return mGainControl != null;
}
/**
* Monitors the source data line playback state and broadcasts audio events
* to the registered listener as the state changes
* Monitors the source data line playback state and broadcasts audio events to the registered listener as the
* state changes
*/
@Override
public void update(LineEvent event)
@@ -454,4 +724,23 @@ public abstract class AudioOutput implements Listener<ReusableAudioPacket>, Line
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
}
}
/**
* Runnable audio segment processor
*/
public class AudioSegmentProcessor implements Runnable
{
@Override
public void run()
{
try
{
processAudio();
}
catch(Throwable t)
{
mLog.error("Error while processing audio buffers", t);
}
}
}
}

View File

@@ -23,17 +23,24 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioException;
import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.PreferenceEditorViewRequest;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.FloatControl;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JMenu;
@@ -51,6 +58,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.List;
public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
@@ -169,34 +177,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
JPopupMenu popup = new JPopupMenu();
/* Audio mixer/output selection menus */
JMenu outputMenu = new JMenu("Audio Output");
MixerChannelConfiguration[] mixerConfigurations =
mSourceManager.getMixerManager().getOutputMixers();
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
JMenuItem outputMenu = new JMenuItem("Configure ...");
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
outputMenu.setIcon(icon);
outputMenu.addActionListener(new ActionListener()
{
MixerSelectionItem mixerItem = new MixerSelectionItem(mixerConfig);
try
@Override
public void actionPerformed(ActionEvent e)
{
MixerChannelConfiguration current = mController.getMixerChannelConfiguration();
if(current != null && current.equals(mixerConfig))
{
mixerItem.setSelected(true);
}
MyEventBus.getEventBus().post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK));
}
catch(AudioException e)
{
mLog.error("Error while detecting current mixer "
+ "channel configuration", e);
}
outputMenu.add(mixerItem);
}
});
popup.add(outputMenu);
popup.add(new JPopupMenu.Separator());
/* Audio output mute and volume control */
for(AudioOutput output : mController.getAudioOutputs())

View File

@@ -19,13 +19,17 @@
*/
package io.github.dsheirer.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioException;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
@@ -38,113 +42,206 @@ import javax.sound.sampled.Mixer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAudioController
/**
* Manages scheduling and playback of audio segments to the local users audio system.
*/
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
{
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
public static final int AUDIO_TIMEOUT = 1000; //1 second
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
private LinkedTransferQueue<ReusableAudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
private AudioOutputConnection mLowestPriorityConnection;
private int mAvailableConnectionCount;
private Map<String,AudioOutput> mAudioOutputMap = new HashMap<>();
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ScheduledFuture<?> mProcessingTask;
private MixerManager mMixerManager;
private UserPreferences mUserPreferences;
private MixerChannelConfiguration mMixerChannelConfiguration;
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
/**
* Processes all audio produced by the decoding channels and routes audio
* packets to any combination of outputs based on any alias audio routing
* options specified by the user.
* Constructs an instance.
*
* @param userPreferences for audio playback preferences
*/
public AudioPlaybackManager(MixerManager mixerManager)
public AudioPlaybackManager(UserPreferences userPreferences)
{
mMixerManager = mixerManager;
mUserPreferences = userPreferences;
MyEventBus.getEventBus().register(this);
loadSettings();
}
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
/**
* Loads the saved mixer configuration or a default configuration for audio playback.
*/
private void loadSettings()
{
MixerChannelConfiguration configuration = null;
SystemProperties properties = SystemProperties.getInstance();
Mixer defaultMixer = AudioSystem.getMixer(null);
String mixer = properties.get(AUDIO_MIXER_PROPERTY, defaultMixer.getMixerInfo().getName());
String channels = properties.get(AUDIO_CHANNELS_PROPERTY, MixerChannel.MONO.name());
MixerChannelConfiguration[] mixerConfigurations = mMixerManager.getOutputMixers();
for(MixerChannelConfiguration mixerConfig : mixerConfigurations)
if(configuration != null)
{
if(mixerConfig.matches(mixer, channels))
{
configuration = mixerConfig;
}
}
if(configuration == null)
{
configuration = getDefaultConfiguration();
}
try
{
setMixerChannelConfiguration(configuration, false);
}
catch(Exception e)
{
mLog.error("Couldn't set stored audio mixer/channel configuration - using default", e);
try
{
setMixerChannelConfiguration(getDefaultConfiguration());
setMixerChannelConfiguration(configuration);
}
catch(Exception e2)
catch(AudioException ae)
{
mLog.error("Couldn't set default audio mixer/channel configuration - no audio will be available", e2);
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
}
}
else
{
mLog.warn("No audio output devices available");
}
}
/**
* Creates a default audio playback configuration with a mono audio playback channel.
* Receives audio segments from channel audio modules.
* @param audioSegment
*/
private MixerChannelConfiguration getDefaultConfiguration()
@Override
public void receive(AudioSegment audioSegment)
{
/* Use the system default mixer and mono channel as default startup */
Mixer defaultMixer = AudioSystem.getMixer(null);
mNewAudioSegmentQueue.add(audioSegment);
}
return new MixerChannelConfiguration(defaultMixer, MixerChannel.MONO);
/**
* Processes new audio segments and automatically assigns them to audio outputs.
*
* Note: this method is intended to be repeatedly invoked by a scheduled processing thread.
*/
private void processAudioSegments()
{
//Process new audio segments queue. If segment has audio, queue it for replay, otherwise place in pending queue
AudioSegment newSegment = mNewAudioSegmentQueue.poll();
while(newSegment != null)
{
if(newSegment.hasAudio())
{
mAudioSegments.add(newSegment);
}
else
{
mPendingAudioSegments.add(newSegment);
}
newSegment = mNewAudioSegmentQueue.poll();
}
//Transfer pending audio segments that now have audio or that completed without ever having audio
if(!mPendingAudioSegments.isEmpty())
{
Iterator<AudioSegment> it = mPendingAudioSegments.iterator();
AudioSegment audioSegment;
while(it.hasNext())
{
audioSegment = it.next();
if(audioSegment.hasAudio())
{
//Queue it up for replay
it.remove();
mAudioSegments.add(audioSegment);
}
else if(audioSegment.completeProperty().get())
{
//Rare situation: the audio segment completed but never had audio ... dispose it
it.remove();
audioSegment.decrementConsumerCount();
}
}
}
//Process all audio segments that have audio
if(!mAudioSegments.isEmpty())
{
Iterator<AudioSegment> it = mAudioSegments.iterator();
AudioSegment audioSegment;
//Remove any audio segments flagged as do not monitor. Don't remove completed segments yet, because
//we want to give them a brief chance at playback. Automatically assign linked audio segments to the
//current audio output for audio continuity
while(it.hasNext())
{
audioSegment = it.next();
if(audioSegment.isDoNotMonitor())
{
it.remove();
audioSegment.decrementConsumerCount();
}
else if(audioSegment.isLinked())
{
for(AudioOutput audioOutput: mAudioOutputs)
{
if(audioOutput.isLinkedTo(audioSegment))
{
it.remove();
audioOutput.play(audioSegment);
}
}
}
}
//Sort audio segments by playback priority and assign to empty audio outputs
if(!mAudioSegments.isEmpty())
{
Collections.sort(mAudioSegments, Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
{
if(audioOutput.emptyProperty().get())
{
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
{
return;
}
}
}
}
//Preempt ongoing audio segment playback when higher priority audio segments are available
if(!mAudioSegments.isEmpty())
{
for(AudioOutput audioOutput: mAudioOutputs)
{
if(mAudioSegments.get(0).monitorPriorityProperty().get() < audioOutput.audioPriorityProperty().get())
{
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
{
return;
}
}
}
}
//Remove any audio segments marked as complete that didn't get assigned to an output
it = mAudioSegments.iterator(); //reset the iterator
while(it.hasNext())
{
audioSegment = it.next();
if(audioSegment.completeProperty().get())
{
it.remove();
audioSegment.decrementConsumerCount();
}
}
}
}
public void dispose()
@@ -152,68 +249,34 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
if(mProcessingTask != null)
{
mProcessingTask.cancel(true);
mProcessingTask = null;
}
mAudioPacketQueue.clear();
mProcessingTask = null;
mChannelConnectionMap.clear();
for(AudioOutputConnection connection : mAudioOutputConnections)
{
connection.dispose();
}
mAudioOutputConnections.clear();
mNewAudioSegmentQueue.clear();
mAudioSegments.clear();
}
/**
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
* Receive user preference update notifications so that we can detect when the user changes the audio output
* device in the user preferences editor.
*/
@Override
public synchronized void receive(ReusableAudioPacket packet)
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
mAudioPacketQueue.add(packet);
}
/**
* Checks each audio channel assignment and disconnects any inactive connections
*/
private void disconnectInactiveChannelAssignments()
{
boolean changed = false;
for(AudioOutputConnection connection : mAudioOutputConnections)
if(preferenceType == PreferenceType.PLAYBACK)
{
if(connection.isInactive() && mChannelConnectionMap.containsKey(connection.getAudioChannelId()))
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
if(configuration != null && !configuration.equals(mMixerChannelConfiguration))
{
mChannelConnectionMap.remove(connection.getAudioChannelId());
connection.disconnect();
mAvailableConnectionCount++;
changed = true;
}
}
if(changed)
{
updateLowestPriorityAssignment();
}
}
/**
* Identifies the lowest priority channel connection where the a higher value indicates a lower priority.
*/
private void updateLowestPriorityAssignment()
{
mLowestPriorityConnection = null;
for(AudioOutputConnection connection : mAudioOutputConnections)
{
if(connection.isConnected() &&
(mLowestPriorityConnection == null || mLowestPriorityConnection.getPriority() < connection.getPriority()))
{
mLowestPriorityConnection = connection;
try
{
setMixerChannelConfiguration(configuration);
}
catch(AudioException ae)
{
mLog.error("Error changing audio output to [" + configuration.toString() + "]", ae);
}
}
}
}
@@ -227,19 +290,7 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
@Override
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
{
setMixerChannelConfiguration(entry, true);
}
/**
* Configures audio playback to use the configuration specified in the entry argument.
*
* @param entry to use in configuring the audio playback setup.
* @param saveSettings to save the audio playback configuration settings in the properties file.
* @throws AudioException if there is an error
*/
public void setMixerChannelConfiguration(MixerChannelConfiguration entry, boolean saveSettings) throws AudioException
{
if(entry != null && (entry.getMixerChannel() == MixerChannel.MONO || entry.getMixerChannel() == MixerChannel.STEREO))
if(entry != null)
{
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
@@ -248,68 +299,37 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
mProcessingTask.cancel(true);
}
disposeCurrentConfiguration();
for(AudioOutput audioOutput: mAudioOutputs)
{
audioOutput.dispose();
}
mAudioOutputs.clear();
switch(entry.getMixerChannel())
{
case MONO:
AudioOutput mono = new MonoAudioOutput(entry.getMixer());
mAudioOutputConnections.add(new AudioOutputConnection(mono));
mAvailableConnectionCount++;
mAudioOutputMap.put(mono.getChannelName(), mono);
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
mAudioOutputs.add(mono);
break;
case STEREO:
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT);
mAudioOutputConnections.add(new AudioOutputConnection(left));
mAvailableConnectionCount++;
mAudioOutputMap.put(left.getChannelName(), left);
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
mAudioOutputs.add(left);
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT);
mAudioOutputConnections.add(new AudioOutputConnection(right));
mAvailableConnectionCount++;
mAudioOutputMap.put(right.getChannelName(), right);
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
mAudioOutputs.add(right);
break;
default:
throw new AudioException("Unsupported mixer channel "
+ "configuration: " + entry.getMixerChannel());
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
}
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioPacketProcessor(),
0, 15, TimeUnit.MILLISECONDS);
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
if(saveSettings)
{
SystemProperties properties = SystemProperties.getInstance();
properties.set(AUDIO_MIXER_PROPERTY, entry.getMixer().getMixerInfo().getName());
properties.set(AUDIO_CHANNELS_PROPERTY, entry.getMixerChannel().name());
}
mMixerChannelConfiguration = entry;
}
}
/**
* Clears all channel assignments and terminates all audio outputs in preparation for complete shutdown or change
* to another mixer/channel configuration
*/
private void disposeCurrentConfiguration()
{
mChannelConnectionMap.clear();
for(AudioOutputConnection connection : mAudioOutputConnections)
{
connection.dispose();
}
mAvailableConnectionCount = 0;
mAudioOutputConnections.clear();
mAudioOutputMap.clear();
mLowestPriorityConnection = null;
}
/**
* Current audio playback mixer channel configuration setting.
*/
@@ -320,12 +340,12 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
}
/**
* List of audio outputs available for the current mixer channel configuration
* List of sorted audio outputs available for the current mixer channel configuration
*/
@Override
public List<AudioOutput> getAudioOutputs()
{
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputMap.values());
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputs);
Collections.sort(outputs, new Comparator<AudioOutput>()
{
@@ -358,228 +378,21 @@ public class AudioPlaybackManager implements Listener<ReusableAudioPacket>, IAud
}
/**
* Returns an audio output connection for the packet if one is available, or overrides an existing lower priority
* connection. Returns null if no connection is available for the audio packet.
*
* @param audioPacket from a decoding channel source
* @return an audio output connection or null
* Scheduled runnable to process incoming audio segments
*/
private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket)
public class AudioSegmentProcessor implements Runnable
{
int audioChannelId = audioPacket.getAudioChannelId();
//Use an existing connection
if(mChannelConnectionMap.containsKey(audioChannelId))
{
return mChannelConnectionMap.get(audioChannelId);
}
//Connect to an unused, available connection
if(mAvailableConnectionCount > 0)
{
for(AudioOutputConnection connection : mAudioOutputConnections)
{
if(connection.isDisconnected())
{
connection.connect(audioChannelId, audioPacket.getMonitoringPriority());
mChannelConnectionMap.put(audioChannelId, connection);
mAvailableConnectionCount--;
return connection;
}
}
}
//Preempt an existing lower priority connection and connect when this is a higher priority packet
else
{
int priority = audioPacket.getMonitoringPriority();
AudioOutputConnection connection = mLowestPriorityConnection;
if(connection != null && priority < connection.getPriority())
{
mChannelConnectionMap.remove(connection.getAudioChannelId());
connection.connect(audioChannelId, priority);
mChannelConnectionMap.put(audioChannelId, connection);
return connection;
}
}
return null;
}
public class AudioPacketProcessor implements Runnable
{
private List<ReusableAudioPacket> mPackets = new ArrayList<>();
@Override
public void run()
{
try
{
disconnectInactiveChannelAssignments();
if(mAudioPacketQueue != null)
{
mAudioPacketQueue.drainTo(mPackets);
for(ReusableAudioPacket packet : mPackets)
{
/* Don't process any packet's marked as do not monitor */
if(!packet.isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO)
{
AudioOutputConnection connection = getConnection(packet);
if(connection != null)
{
connection.receive(packet);
}
else
{
packet.decrementUserCount();
}
}
else
{
packet.decrementUserCount();
}
}
mPackets.clear();
}
processAudioSegments();
}
catch(Exception e)
catch(Throwable t)
{
mLog.error("Encountered error while processing audio packets", e);
while(mPackets.size() > 0)
{
ReusableAudioPacket audioPacket = mPackets.remove(0);
audioPacket.decrementUserCount();
}
mLog.error("Encountered error while processing audio segments", t);
}
}
}
/**
* Audio output connection manages a connection between a source and an audio
* output and maintains current state information about the audio activity
* received form the source.
*/
public class AudioOutputConnection
{
private static final int DISCONNECTED = Integer.MIN_VALUE;
private AudioOutput mAudioOutput;
private int mPriority = 0;
private int mAudioChannelId = DISCONNECTED;
public AudioOutputConnection(AudioOutput audioOutput)
{
mAudioOutput = audioOutput;
}
public void receive(ReusableAudioPacket packet)
{
if(packet.hasIdentifierCollection() && packet.getAudioChannelId() == mAudioChannelId)
{
int priority = packet.getMonitoringPriority();
if(mPriority != priority)
{
mPriority = priority;
updateLowestPriorityAssignment();
}
if(mAudioOutput != null)
{
mAudioOutput.receive(packet);
}
}
else
{
if(packet.hasIdentifierCollection())
{
mLog.error("Received audio packet from channel metadata [" + packet.getAudioChannelId() +
"] however this assignment is currently connected to metadata [" + mAudioChannelId + "]");
}
else
{
mLog.error("Received audio packet with no metadata - cannot route audio packet");
}
}
}
/**
* Terminates the audio output and prepares this connection for disposal
*/
public void dispose()
{
mAudioOutput.dispose();
mAudioOutput = null;
}
/**
* Indicates if this assignment is currently disconnected from a channel source
*/
public boolean isDisconnected()
{
return mAudioChannelId == DISCONNECTED;
}
/**
* Indicates if this assignment is currently connected to a channel source
*/
public boolean isConnected()
{
return !isDisconnected();
}
/**
* Connects this assignment to the indicated source so that audio
* packets from this source can be sent to the audio output
*/
public void connect(int source, int priority)
{
mAudioChannelId = source;
mPriority = priority;
updateLowestPriorityAssignment();
mAudioOutput.updateTimestamp();
}
/**
* Currently connected source or -1 if disconnected
*/
public int getAudioChannelId()
{
return mAudioChannelId;
}
/**
* Disconnects this assignment from the source and prevents any audio
* from being routed to the audio output until another source is assigned
*/
public void disconnect()
{
mAudioChannelId = DISCONNECTED;
mPriority = 0;
}
/**
* Indicates if audio output is current inactive, meaning that the
* audio output hasn't recently processed any audio packets.
*/
public boolean isInactive()
{
return (mAudioOutput.getLastActivityTimestamp() + AUDIO_TIMEOUT) < System.currentTimeMillis();
}
public int getPriority()
{
return mPriority;
}
}
}

View File

@@ -19,7 +19,7 @@
package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
@@ -34,23 +34,21 @@ public class MonoAudioOutput extends AudioOutput
{
private final static int BUFFER_SIZE = 8000;
public MonoAudioOutput(Mixer mixer)
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
{
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8KHZ_16BITS_MONO,
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE);
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
}
/**
* Converts the audio packet data into mono audio frames.
*/
protected ByteBuffer convert(ReusableAudioPacket packet)
protected ByteBuffer convert(float[] samples)
{
ByteBuffer buffer = null;
if(packet.hasAudioSamples())
if(samples.length > 0)
{
float[] samples = packet.getAudioSamples();
/* Little-endian byte buffer */
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
@@ -62,8 +60,6 @@ public class MonoAudioOutput extends AudioOutput
}
}
packet.decrementUserCount();
return buffer;
}
}

View File

@@ -19,7 +19,7 @@
package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
@@ -34,10 +34,10 @@ public class StereoAudioOutput extends AudioOutput
{
private final static int BUFFER_SIZE = 16000;
public StereoAudioOutput(Mixer mixer, MixerChannel channel)
public StereoAudioOutput(Mixer mixer, MixerChannel channel, UserPreferences userPreferences)
{
super(mixer, channel, AudioFormats.PCM_SIGNED_8KHZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO,
BUFFER_SIZE);
BUFFER_SIZE, userPreferences);
}
/**
@@ -45,14 +45,12 @@ public class StereoAudioOutput extends AudioOutput
* channel containing the audio and the other channel containing zero
* valued (silent) samples.
*/
protected ByteBuffer convert(ReusableAudioPacket packet)
protected ByteBuffer convert(float[] samples)
{
ByteBuffer buffer = null;
if(packet.hasAudioSamples())
if(samples.length > 0)
{
float[] samples = packet.getAudioSamples();
/* Little-endian byte buffer */
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
@@ -76,8 +74,6 @@ public class StereoAudioOutput extends AudioOutput
}
}
packet.decrementUserCount();
return buffer;
}
}

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
@@ -94,8 +100,8 @@ public class MultiSyncPatternMatcher
if(mBitCount > mSyncLossThreshold)
{
mSyncDetectListener.syncLost(mBitCount);
mBitCount = 0;
mSyncDetectListener.syncLost();
}
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -261,7 +261,12 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
@Override
public void receive(IdentifierUpdateNotification update)
{
// mLog.debug("Received update: " + update + " class:" + update.getIdentifier().getClass());
// mLog.debug("Received update: " + update +
// " class:" + update.getIdentifier().getIdentifierClass() +
// " form:" + update.getIdentifier().getForm() +
// " role:" + update.getIdentifier().getRole() +
// " class:" + update.getIdentifier().getClass());
Identifier identifier = update.getIdentifier();
switch(identifier.getIdentifierClass())

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -199,7 +199,8 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
}
else
{
return id.getValue() + " TS:" + channelMetadata.getTimeslot();
String value = id.getValue().replace(" TS0", "").replace(" TS1", "");
return value + " TS:" + channelMetadata.getTimeslot();
}
}
else

View File

@@ -1,29 +1,33 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.channel.state;
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
import io.github.dsheirer.identifier.string.StringIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.protocol.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -35,6 +39,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
{
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
private static final String NO_SQUELCH = "No Squelch";
private Identifier mChannelNameIdentifier;
private DecoderType mDecoderType;
private String mChannelName;
@@ -42,7 +47,8 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName)
{
mDecoderType = decoderType;
mChannelName = channelName;
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : decoderType.name() + " CHANNEL";
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.USER, Form.CHANNEL_NAME, Role.TO);
}
@Override
@@ -74,9 +80,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
{
if(event.getEvent() == Event.RESET)
{
//Each time we're reset, set the PRIMARY TO attribute back to the channel name, otherwise we won't have
//a primary ID for any audio produced by this state.
// broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
getIdentifierCollection().update(mChannelNameIdentifier);
}
}
@@ -89,8 +93,8 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
@Override
public void start()
{
// broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));
getIdentifierCollection().update(mChannelNameIdentifier);
}
@Override

View File

@@ -105,7 +105,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
//Set the proxy as a listener so that echo'd updates are broadcast externally
mutableIdentifierCollection.setIdentifierUpdateListener(mIdentifierUpdateNotificationProxy);
StateMachine stateMachine = new StateMachine(timeslot);
StateMachine stateMachine = new StateMachine(timeslot, State.MULTI_CHANNEL_ACTIVE_STATES);
mStateMachineMap.put(timeslot, stateMachine);
stateMachine.addListener(this);
@@ -133,7 +133,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
public void stateChanged(State state, int timeslot)
{
//Broadcast current channel state so that channel rotation monitor can track
if(state.isActiveState())
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state))
{
broadcast(DecoderStateEvent.activeState(timeslot));
}
@@ -148,9 +148,6 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
switch(state)
{
case IDLE:
broadcast(new DecoderStateEvent(this, Event.RESET, State.IDLE));
break;
case RESET:
reset(timeslot);
mStateMachineMap.get(timeslot).setState(State.IDLE);
@@ -173,19 +170,49 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
*/
private void checkTeardown()
{
boolean teardown = true;
boolean teardown = false;
boolean active = false;
for(StateMachine stateMachine: mStateMachineMap.values())
{
if(stateMachine.getState().isActiveState())
State state = stateMachine.getState();
//If we have an active state in either timeslot, don't teardown. IDLE is a special state that is active
//but doesn't prevent a teardown when the other timeslot is in TEARDOWN
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(state) && state != State.IDLE)
{
teardown = false;
active = true;
}
else if(state == State.TEARDOWN)
{
teardown = true;
}
}
if(teardown)
if(teardown && !active)
{
broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
if(mChannel.isTrafficChannel())
{
broadcast(new ChannelEvent(mChannel, ChannelEvent.Event.REQUEST_DISABLE));
}
else
{
for(StateMachine stateMachine: mStateMachineMap.values())
{
stateMachine.setState(State.RESET);
}
}
}
//If one timeslot is teardown but the other is still active, reset the teardown timeslot to IDLE
else if(teardown && active)
{
for(StateMachine stateMachine: mStateMachineMap.values())
{
if(stateMachine.getState() == State.TEARDOWN)
{
stateMachine.setState(State.RESET);
}
}
}
}
@@ -340,11 +367,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
{
mIdentifierCollectionMap.get(timeslot).broadcastIdentifiers();
if(mChannel.getChannelType() == ChannelType.TRAFFIC)
{
mStateMachineMap.get(timeslot).setState(State.ACTIVE);
}
mStateMachineMap.get(timeslot).setState(State.RESET);
}
}
@@ -476,7 +499,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
//in an active (ie sync locked) state.
for(int timeslot = 0; timeslot < mTimeslotCount; timeslot++)
{
if(mStateMachineMap.get(timeslot).getState().isActiveState())
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(mStateMachineMap.get(timeslot).getState()))
{
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
mChannel.getChannelType().name()));
@@ -511,7 +534,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
case CONTINUATION:
case DECODE:
case START:
if(State.CALL_STATES.contains(event.getState()))
if(State.MULTI_CHANNEL_ACTIVE_STATES.contains(event.getState()))
{
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
}
@@ -519,11 +542,11 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
case END:
if(mChannel.isTrafficChannel())
{
mStateMachineMap.get(event.getTimeslot()).setState(State.TEARDOWN);
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
}
else
{
mStateMachineMap.get(event.getTimeslot()).setState(State.FADE);
mStateMachineMap.get(event.getTimeslot()).setState(event.getState());
}
break;
case RESET:

View File

@@ -94,7 +94,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
private SourceEventListener mInternalSourceEventListener;
private ChannelMetadata mChannelMetadata;
private StateMachine mStateMachine = new StateMachine(0);
private StateMachine mStateMachine = new StateMachine(0, State.SINGLE_CHANNEL_ACTIVE_STATES);
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
public SingleChannelState(Channel channel, AliasModel aliasModel)
@@ -123,7 +123,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
public void stateChanged(State state, int timeslot)
{
//Broadcast current channel state so that channel rotation monitor can track
if(state.isActiveState())
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(state))
{
broadcast(DecoderStateEvent.activeState(timeslot));
}
@@ -377,7 +377,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//Rebroadcast frequency error measurements to external listener if we're currently
//in an active (ie sync locked) state.
if(mStateMachine.getState().isActiveState())
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(mStateMachine.getState()))
{
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(),
mChannel.getChannelType().name()));
@@ -411,7 +411,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
case CONTINUATION:
case DECODE:
case START:
if(State.CALL_STATES.contains(event.getState()))
if(State.SINGLE_CHANNEL_ACTIVE_STATES.contains(event.getState()))
{
mStateMachine.setState(event.getState());
}

View File

@@ -43,6 +43,7 @@ public enum State
state == DATA ||
state == ENCRYPTED ||
state == FADE ||
state == IDLE ||
state == TEARDOWN ||
state == RESET;
}
@@ -60,6 +61,7 @@ public enum State
state == DATA ||
state == ENCRYPTED ||
state == FADE ||
state == IDLE ||
state == TEARDOWN ||
state == RESET;
}
@@ -121,7 +123,8 @@ public enum State
}
},
/**
* Indicates the channel is idle and not actively decoding any messages
* Indicates the channel is idle and not actively decoding any messages or in a multiple timeslot decoder this
* state reflects active decoding that indicates the timeslot is idle.
*/
IDLE("IDLE")
{
@@ -159,8 +162,8 @@ public enum State
private String mDisplayValue;
public static final EnumSet<State> CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> IDLE_STATES = EnumSet.of(IDLE, FADE);
public static final EnumSet<State> SINGLE_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> MULTI_CHANNEL_ACTIVE_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED, IDLE);
private State(String displayValue)
{
@@ -169,17 +172,6 @@ public enum State
public abstract boolean canChangeTo(State state);
/**
* Indicates that this state is an active (ie decoding) state and is one of the states
* enumerated in the CALL_STATES enumeration set.
*
* @return true if this state is an active state
*/
public boolean isActiveState()
{
return CALL_STATES.contains(this);
}
public String getDisplayValue()
{
return mDisplayValue;

View File

@@ -30,8 +30,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
/**
* State machine for tracking a channel state.
*/
public class StateMachine
{
private final static Logger mLog = LoggerFactory.getLogger(StateMachine.class);
@@ -42,13 +46,21 @@ public class StateMachine
protected long mEndTimeout;
protected long mEndTimeoutBuffer = 0;
private int mTimeslot;
private EnumSet<State> mActiveStates;
private Channel.ChannelType mChannelType = Channel.ChannelType.STANDARD;
private List<IStateMachineListener> mStateMachineListeners = new ArrayList<>();
private Listener<IdentifierUpdateNotification> mIdentifierUpdateListener;
public StateMachine(int timeslot)
/**
* Constructs an instance
*
* @param timeslot for this state machine
* @param activeStates set of states considered active for updating the fade timeout
*/
public StateMachine(int timeslot, EnumSet<State> activeStates)
{
mTimeslot = timeslot;
mActiveStates = activeStates;
}
public void addListener(IStateMachineListener listener)
@@ -73,7 +85,7 @@ public class StateMachine
public void checkState()
{
if(mState.isActiveState() && mFadeTimeout <= System.currentTimeMillis())
if(mActiveStates.contains(mState) && mFadeTimeout <= System.currentTimeMillis())
{
setState(State.FADE);
}
@@ -92,18 +104,22 @@ public class StateMachine
{
if(state == mState)
{
if(State.CALL_STATES.contains(state))
if(mActiveStates.contains(state))
{
updateFadeTimeout();
}
}
else if(mState.canChangeTo(state))
{
if(mActiveStates.contains(state))
{
updateFadeTimeout();
}
switch(state)
{
case ACTIVE:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.ACTIVE);
break;
case CONTROL:
@@ -111,23 +127,19 @@ public class StateMachine
if(mChannelType == Channel.ChannelType.STANDARD)
{
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.CONTROL);
}
break;
case DATA:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.DATA);
break;
case ENCRYPTED:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.ENCRYPTED);
break;
case CALL:
mState = state;
updateFadeTimeout();
broadcast(ChannelStateIdentifier.CALL);
break;
case FADE:

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.controller;
@@ -37,7 +34,6 @@ import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.map.MapPanel;
import io.github.dsheirer.map.MapService;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderManager;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.tuner.TunerModel;
@@ -67,8 +63,7 @@ public class ControllerPanel extends JPanel
public ControllerPanel(AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel, BroadcastModel broadcastModel,
ChannelModel channelModel, ChannelMapModel channelMapModel, ChannelProcessingManager channelProcessingManager,
IconManager iconManager, MapService mapService, SettingsManager settingsManager,
SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences,
RecorderManager recorderManager)
SourceManager sourceManager, TunerModel tunerModel, UserPreferences userPreferences)
{
mBroadcastModel = broadcastModel;
mChannelModel = channelModel;
@@ -88,7 +83,7 @@ public class ControllerPanel extends JPanel
mAliasController = new AliasController(aliasModel, broadcastModel, iconManager, userPreferences);
mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences, recorderManager);
mTunerManagerPanel = new TunerViewPanel(tunerModel, userPreferences);
init();
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -22,6 +22,7 @@
package io.github.dsheirer.controller.channel;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.channel.IChannelDescriptor;
import io.github.dsheirer.channel.metadata.ChannelMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
@@ -42,11 +43,10 @@ import io.github.dsheirer.module.decode.event.MessageActivityModel;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.RecorderFactory;
import io.github.dsheirer.record.RecorderManager;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.SourceManager;
import org.slf4j.Logger;
@@ -70,14 +70,13 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
private Map<Channel,ProcessingChain> mProcessingChains = new HashMap<>();
private List<Listener<ReusableAudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
private List<Listener<AudioSegment>> mAudioSegmentListeners = new CopyOnWriteArrayList<>();
private List<Listener<IDecodeEvent>> mDecodeEventListeners = new CopyOnWriteArrayList<>();
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
private ChannelMapModel mChannelMapModel;
private ChannelMetadataModel mChannelMetadataModel;
private EventLogManager mEventLogManager;
private RecorderManager mRecorderManager;
private SourceManager mSourceManager;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
@@ -87,18 +86,15 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*
* @param channelMapModel containing channel maps defined by the user
* @param eventLogManager for adding event loggers to channels
* @param recorderManager for receiving audio packets produced by the channel
* @param sourceManager for obtaining a tuner channel source for the channel
* @param aliasModel for aliasing of identifiers produced by the channel
* @param userPreferences for user defined behavior and settings
*/
public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager,
RecorderManager recorderManager, SourceManager sourceManager,
AliasModel aliasModel, UserPreferences userPreferences)
SourceManager sourceManager, AliasModel aliasModel, UserPreferences userPreferences)
{
mChannelMapModel = channelMapModel;
mEventLogManager = eventLogManager;
mRecorderManager = recorderManager;
mSourceManager = sourceManager;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
@@ -252,9 +248,9 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
mChannelEventBroadcaster.addListener(processingChain);
/* Register global listeners */
for(Listener<ReusableAudioPacket> listener : mAudioPacketListeners)
for(Listener<AudioSegment> listener : mAudioSegmentListeners)
{
processingChain.addAudioPacketListener(listener);
processingChain.addAudioSegmentListener(listener);
}
for(Listener<IDecodeEvent> listener : mDecodeEventListeners)
@@ -262,6 +258,30 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
processingChain.addDecodeEventListener(listener);
}
//Add a listener to detect source error state that indicates the channel should be shutdown
processingChain.addSourceEventListener(sourceEvent ->
{
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE && sourceEvent.getSource() != null)
{
Channel toShutdown = null;
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChains.entrySet())
{
if(entry.getValue().hasSource(sourceEvent.getSource()))
{
toShutdown = entry.getKey();
break;
}
}
if(toShutdown != null)
{
mLog.info("Channel source error detected - stopping channel [" + toShutdown.getName() + "]");
stopProcessing(toShutdown, true);
}
}
});
//Register this manager to receive channel events from traffic channel manager modules within
//the processing chain
processingChain.addChannelEventListener(this);
@@ -289,7 +309,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
//Add recorders
processingChain.addModules(RecorderFactory.getRecorders(mRecorderManager, mUserPreferences, channel));
processingChain.addModules(RecorderFactory.getRecorders(mUserPreferences, channel));
//Set the samples source
processingChain.setSource(source);
@@ -398,13 +418,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
public void shutdown()
{
mLog.debug("Stopping Channels ...");
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
for(Channel channel : channelsToStop)
{
mLog.debug("Stopping channel: " + channel.toString());
stopProcessing(channel, true);
}
}
@@ -413,17 +430,17 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
* Adds a message listener that will be added to all channels to receive
* any messages.
*/
public void addAudioPacketListener(Listener<ReusableAudioPacket> listener)
public void addAudioSegmentListener(Listener<AudioSegment> listener)
{
mAudioPacketListeners.add(listener);
mAudioSegmentListeners.add(listener);
}
/**
* Removes a message listener.
*/
public void removeAudioPacketListener(Listener<ReusableAudioPacket> listener)
public void removeAudioSegmentListener(Listener<AudioSegment> listener)
{
mAudioPacketListeners.remove(listener);
mAudioSegmentListeners.remove(listener);
}
/**

View File

@@ -28,6 +28,7 @@ import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -62,7 +63,7 @@ import java.util.List;
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("0.0");
private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 25000;
//Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers
@@ -154,7 +155,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
channels--;
}
mLog.debug("Sample Rate [" + sampleRate + "] providing [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] Hz each");
mLog.info("Sample Rate [" + DECIMAL_FORMAT.format(sampleRate) + "] providing [" + channels +
"] channels at [" + DECIMAL_FORMAT.format(sampleRate / (double)channels) + "] Hz each");
return channels;
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -126,6 +126,18 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
}
/**
* Signals to all provisioned tuner channel sources that the source complex buffer provider has an error and can
* no longer provide channels, so that the tuner channel source can notify the consumer of the error state.
*/
public void setErrorMessage(String errorMessage)
{
for(TunerChannelSource tunerChannelSource: mChannelSources)
{
tunerChannelSource.setError(errorMessage);
}
}
/**
* Current channel sample rate which is (2 * channel bandwidth).
*/

View File

@@ -1,21 +1,23 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
*/
package io.github.dsheirer.dsp.fsk;
@@ -139,7 +141,7 @@ public class LTRDecoder implements Listener<ReusableFloatBuffer>, ISyncDetectLis
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
//no-op
}

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.dsp.fsk;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
@@ -129,7 +135,7 @@ public class SynchronizationMonitor implements ISyncDetectListener
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
//no-op
}

View File

@@ -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));
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.sample.complex.Complex;
@@ -41,7 +47,7 @@ public class CostasLoop implements IPhaseLockedLoop
private double mDamping = Math.sqrt(2.0) / 2.0;
private double mAlphaGain;
private double mBetaGain;
private PLLGain mPLLGain = PLLGain.LEVEL_1;
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
private double mSymbolRate;
private double mSampleRate;
private IFrequencyErrorProcessor mFrequencyErrorProcessor;
@@ -63,8 +69,7 @@ public class CostasLoop implements IPhaseLockedLoop
}
/**
* Registers the listener to receive the measured frequency error as tracked by the PLL at one
* second intervals.
* Registers the listener to receive the measured frequency error as tracked by the PLL once per second
*
* @param listener to receive frequency error measurement.
*/
@@ -102,22 +107,25 @@ public class CostasLoop implements IPhaseLockedLoop
*/
private void updateLoopBandwidth()
{
double bandwidth = TWO_PI / mPLLGain.getLoopBandwidth();
double bandwidth = TWO_PI / mPLLBandwidth.getLoopBandwidth();
mAlphaGain = (4.0 * mDamping * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
mBetaGain = (4.0 * bandwidth * bandwidth) / (1.0 + (2.0 * mDamping * bandwidth) + (bandwidth * bandwidth));
}
/**
* Sets the PLLGain state for this costas loop. The PLLGain state affects the aggressiveness of the alpha/beta
* Sets the PLLBandwidth state for this costas loop. The bandwidth affects the aggressiveness of the alpha/beta
* gain values in synchronizing with the signal carrier.
*
* @param PLLGain
* @param pllBandwidth
*/
public void setPLLGain(PLLGain PLLGain)
public void setPLLBandwidth(PLLBandwidth pllBandwidth)
{
mPLLGain = PLLGain;
updateLoopBandwidth();
if(mPLLBandwidth != pllBandwidth)
{
mPLLBandwidth = pllBandwidth;
updateLoopBandwidth();
}
}
/**
@@ -162,8 +170,7 @@ public class CostasLoop implements IPhaseLockedLoop
}
/**
* Updates the costas loop frequency and phase to adjust for the phase
* error value
* Updates the costas loop frequency and phase to adjust for the phase error value
*
* @param phaseError - (-)= late and (+)= early
*/

View File

@@ -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));
}
}
}

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -21,37 +21,45 @@
*/
package io.github.dsheirer.dsp.psk.pll;
import java.util.Map;
import java.util.TreeMap;
/**
* Phase Locked Loop (PLL) tracking bandwidth - controls how fast/slow the PLL tracks the carrier frequency
*/
public enum PLLGain
public enum PLLBandwidth
{
//NOTE: static gain level of 200 produced the best results for releases prior to 0.3.4b2
//NOTE: static bandwidth of 200 produced the best results for P25P1 releases prior to 0.3.4b2
LEVEL_0(100.0, 0, 0),
LEVEL_1(150.0, 0, 1),
LEVEL_2(170.0, 2, 4),
LEVEL_3(190.0, 5, 6),
LEVEL_4(200.0, 7, 8),
LEVEL_5(200.0, 9, 10),
LEVEL_6(250.0, 11, 12),
LEVEL_7(275.0, 12,13),
LEVEL_8(325.0, 14, 15),
LEVEL_9(350.0, 16,17),
LEVEL_10(375.0, 18, 19),
LEVEL_11(400.0, 19,20);
BW_400(400.0, 0, 1),
BW_300(300.0, 2, 3),
BW_250(250.0, 4, 5),
BW_200(200.0, 6, 7);
private double mLoopBandwidth;
private int mRangeStart;
private int mRangeEnd;
PLLGain(double loopBandwidth, int start, int end)
PLLBandwidth(double loopBandwidth, int start, int end)
{
mLoopBandwidth = loopBandwidth;
mRangeStart = start;
mRangeEnd = end;
}
private static Map<Integer,PLLBandwidth> LOOKUP_MAP = new TreeMap<>();
static
{
for(PLLBandwidth pllBandwidth : PLLBandwidth.values())
{
for(int x = pllBandwidth.getRangeStart(); x <= pllBandwidth.getRangeEnd(); x++)
{
LOOKUP_MAP.put(x, pllBandwidth);
}
}
}
/**
* Loop bandwidth setting
*/
@@ -76,27 +84,13 @@ public enum PLLGain
* @param syncCount 0-10
* @return PLL gain appropriate for the sync detection value
*/
public static PLLGain fromSyncCount(int syncCount)
public static PLLBandwidth fromSyncCount(int syncCount)
{
if(syncCount <= LEVEL_1.getRangeEnd())
if(LOOKUP_MAP.containsKey(syncCount))
{
return LEVEL_1;
}
else if(syncCount <= LEVEL_2.getRangeEnd())
{
return LEVEL_2;
}
else if(syncCount <= LEVEL_3.getRangeEnd())
{
return LEVEL_3;
}
else if(syncCount <= LEVEL_4.getRangeEnd())
{
return LEVEL_4;
}
else
{
return LEVEL_5;
return LOOKUP_MAP.get(syncCount);
}
return BW_200;
}
}

View File

@@ -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);
}
}

View File

@@ -1,3 +1,25 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.dsp.symbol;
public enum Dibit
@@ -133,6 +155,89 @@ public enum Dibit
return Dibit.D00_PLUS_1;
}
public static enum Rotation {PLUS90, MINUS90, INVERT}
/**
* Returns the value of this dibit if the symbol were rotated in the direction indicated.
*
* This can be used for calculating phase rotation and phase inversion pattern derivatives from a normal sync pattern
* @param rotation to apply to this dibit
* @return rotated version of this dibit
*/
public Dibit rotate(Rotation rotation)
{
switch(this)
{
case D00_PLUS_1:
switch(rotation)
{
case PLUS90:
return Dibit.D01_PLUS_3;
case MINUS90:
return Dibit.D10_MINUS_1;
case INVERT:
return Dibit.D11_MINUS_3;
}
break;
case D01_PLUS_3:
switch(rotation)
{
case PLUS90:
return Dibit.D11_MINUS_3;
case MINUS90:
return Dibit.D00_PLUS_1;
case INVERT:
return Dibit.D10_MINUS_1;
}
break;
case D10_MINUS_1:
switch(rotation)
{
case PLUS90:
return Dibit.D00_PLUS_1;
case MINUS90:
return Dibit.D11_MINUS_3;
case INVERT:
return Dibit.D01_PLUS_3;
}
break;
case D11_MINUS_3:
switch(rotation)
{
case PLUS90:
return Dibit.D10_MINUS_1;
case MINUS90:
return Dibit.D01_PLUS_3;
case INVERT:
return Dibit.D00_PLUS_1;
}
}
//We should never reach this point
return Dibit.D00_PLUS_1;
}
/**
* Returns the dibit that corresponds to the value
* @param value 0-3
* @return representative dibit or Dibit.00 if the value is outside of the range 0-3
*/
public static Dibit fromValue(int value)
{
switch(value)
{
case 0:
default:
return Dibit.D00_PLUS_1;
case 1:
return Dibit.D01_PLUS_3;
case 2:
return Dibit.D10_MINUS_1;
case 3:
return Dibit.D11_MINUS_3;
}
}
public static void main(String[] args)
{
byte a = (byte)0x1B;

View File

@@ -1,7 +1,7 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
@@ -29,7 +29,10 @@ public enum FrameSync
P25_PHASE1_ERROR_90_CW( 0x001050551155l ),
P25_PHASE1_ERROR_180( 0xAA8A0A008800l ),
P25_PHASE2_NORMAL(0x575D57F7FFl);
P25_PHASE2_NORMAL( 0x575D57F7FFl),
P25_PHASE2_ERROR_90_CCW(0xFEFBFEAEAAl),
P25_PHASE2_ERROR_90_CW( 0x0104015155l),
P25_PHASE2_ERROR_180( 0xA8A2A80800l);
private long mSync;

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.dsp.symbol;
/**
@@ -31,6 +37,8 @@ public interface ISyncDetectListener
/**
* Indicates that sync has been lost
*
* @param bitsProcessed number of bits that were processed without a sync detection
*/
void syncLost();
void syncLost(int bitsProcessed);
}

View File

@@ -47,7 +47,7 @@ public class JavaFxWindowManager
}
/**
* Closes all JavaFX windows and shutsdown the FX thread
* Closes all JavaFX windows and shuts down the FX thread
*/
public void shutdown()
{
@@ -84,6 +84,8 @@ public class JavaFxWindowManager
{
Stage stage = mPreferencesEditor.getStage();
stage.show();
stage.toFront();
stage.requestFocus();
mPreferencesEditor.showEditor(request);
}
catch(Throwable t)

View File

@@ -1,30 +1,28 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui;
import com.jidesoft.plaf.LookAndFeelFactory;
import com.jidesoft.swing.JideSplitPane;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioPacketManager;
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
@@ -45,7 +43,7 @@ import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.record.RecorderManager;
import io.github.dsheirer.record.AudioRecordingManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.SourceManager;
@@ -60,6 +58,7 @@ import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconFontFX;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
@@ -108,7 +107,8 @@ public class SDRTrunk implements Listener<TunerEvent>
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
private boolean mBroadcastStatusVisible;
private AudioPacketManager mAudioPacketManager;
private AudioRecordingManager mAudioRecordingManager;
private AudioStreamingManager mAudioStreamingManager;
private IconManager mIconManager;
private BroadcastStatusPanel mBroadcastStatusPanel;
private BroadcastModel mBroadcastModel;
@@ -164,6 +164,7 @@ public class SDRTrunk implements Listener<TunerEvent>
//Register FontAwesome so we can use the fonts in Swing windows
IconFontSwing.register(FontAwesome.getIconFont());
IconFontFX.register(FontAwesome.getIconFont());
TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
TunerModel tunerModel = new TunerModel(tunerConfigurationModel);
@@ -178,16 +179,12 @@ public class SDRTrunk implements Listener<TunerEvent>
ChannelMapModel channelMapModel = new ChannelMapModel();
EventLogManager eventLogManager = new EventLogManager(mUserPreferences);
RecorderManager recorderManager = new RecorderManager(aliasModel, mUserPreferences);
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences);
mSourceManager = new SourceManager(tunerModel, mSettingsManager, mUserPreferences);
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, recorderManager,
mSourceManager, aliasModel, mUserPreferences);
mChannelProcessingManager = new ChannelProcessingManager(channelMapModel, eventLogManager, mSourceManager,
aliasModel, mUserPreferences);
mChannelModel.addListener(mChannelProcessingManager);
mChannelProcessingManager.addChannelEventListener(mChannelModel);
@@ -195,25 +192,26 @@ public class SDRTrunk implements Listener<TunerEvent>
ChannelSelectionManager channelSelectionManager = new ChannelSelectionManager(mChannelModel);
mChannelModel.addListener(channelSelectionManager);
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mSourceManager.getMixerManager());
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mUserPreferences);
mAudioRecordingManager = new AudioRecordingManager(mUserPreferences);
mAudioRecordingManager.start();
mBroadcastModel = new BroadcastModel(aliasModel, mIconManager, mUserPreferences);
mAudioStreamingManager = new AudioStreamingManager(mBroadcastModel, BroadcastFormat.MP3,
mUserPreferences);
mAudioStreamingManager.start();
//Audio packets are routed through the audio packet manager for metadata enrichment and then
//distributed to the audio packet processors (ie playback, recording, streaming, etc.)
mAudioPacketManager = new AudioPacketManager(aliasModel);
mAudioPacketManager.addListener(recorderManager);
mAudioPacketManager.addListener(audioPlaybackManager);
mAudioPacketManager.addListener(mBroadcastModel);
mAudioPacketManager.start();
mChannelProcessingManager.addAudioPacketListener(mAudioPacketManager);
mChannelProcessingManager.addAudioSegmentListener(audioPlaybackManager);
mChannelProcessingManager.addAudioSegmentListener(mAudioRecordingManager);
mChannelProcessingManager.addAudioSegmentListener(mAudioStreamingManager);
MapService mapService = new MapService(mIconManager);
mChannelProcessingManager.addDecodeEventListener(mapService);
mControllerPanel = new ControllerPanel(audioPlaybackManager, aliasModel, mBroadcastModel,
mChannelModel, channelMapModel, mChannelProcessingManager, mIconManager,
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences, recorderManager);
mapService, mSettingsManager, mSourceManager, tunerModel, mUserPreferences);
mSpectralPanel = new SpectralDisplayPanel(mChannelModel,
mChannelProcessingManager, mSettingsManager, tunerModel);
@@ -411,7 +409,7 @@ public class SDRTrunk implements Listener<TunerEvent>
viewMenu.add(new JSeparator());
JMenuItem preferencesItem = new JMenuItem("Preferences");
preferencesItem.addActionListener(e -> MyEventBus.getEventBus()
.post(new PreferenceEditorViewRequest(PreferenceEditorType.CHANNEL_EVENT)));
.post(new PreferenceEditorViewRequest(PreferenceEditorType.AUDIO_PLAYBACK)));
viewMenu.add(preferencesItem);
menuBar.add(viewMenu);
@@ -471,10 +469,10 @@ public class SDRTrunk implements Listener<TunerEvent>
mJavaFxWindowManager.shutdown();
mLog.info("Stopping channels ...");
mChannelProcessingManager.shutdown();
mAudioPacketManager.stop();
mAudioRecordingManager.stop();
mLog.info("Stopping spectral display ...");
mSpectralPanel.clearTuner();
mLog.info("Releasing tuners ...");
mSourceManager.shutdown();
mLog.info("Shutdown complete.");
mApplicationLog.stop();

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.gui.instrument.decoder;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
@@ -46,7 +52,7 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
//no-op
}
@@ -60,7 +66,7 @@ public class LJ1200Pane extends AbstractAFSK1200Pane
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
//no-op
}

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.gui.instrument.decoder;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
@@ -46,7 +52,7 @@ public class MDC1200Pane extends AbstractAFSK1200Pane
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
mLog.debug("Sync Lost!");
}

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.gui.instrument.decoder;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
@@ -46,7 +52,7 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
//no-op
}
@@ -60,7 +66,7 @@ public class MPT1327Pane extends AbstractAFSK1200Pane
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
//no-op
}

View File

@@ -1,18 +1,24 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
*/
package io.github.dsheirer.gui.instrument.decoder;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
@@ -46,7 +52,7 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
}
@@ -60,7 +66,7 @@ public class Tait1200Pane extends AbstractAFSK1200Pane
}
@Override
public void syncLost()
public void syncLost(int bitsProcessed)
{
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,13 +14,15 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.gui.preference;
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
import io.github.dsheirer.gui.preference.playback.PlaybackPreferenceEditor;
import io.github.dsheirer.gui.preference.record.RecordPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.ChannelMultipleFrequencyPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
import io.github.dsheirer.preference.UserPreferences;
@@ -36,18 +37,22 @@ public class PreferenceEditorFactory
{
switch(preferenceEditorType)
{
case AUDIO_PLAYBACK:
return new PlaybackPreferenceEditor(userPreferences);
case AUDIO_RECORD:
return new RecordPreferenceEditor(userPreferences);
case CHANNEL_EVENT:
return new DecodeEventViewPreferenceEditor(userPreferences);
case JMBE_LIBRARY:
return new JmbeLibraryPreferenceEditor(userPreferences);
case DIRECTORY:
return new DirectoryPreferenceEditor(userPreferences);
case JMBE_LIBRARY:
return new JmbeLibraryPreferenceEditor(userPreferences);
case SOURCE_CHANNEL_MULTIPLE_FREQUENCY:
return new ChannelMultipleFrequencyPreferenceEditor(userPreferences);
case TALKGROUP_FORMAT:
return new TalkgroupFormatPreferenceEditor(userPreferences);
case SOURCE_TUNER_CHANNELIZER:
return new TunerPreferenceEditor(userPreferences);
case TALKGROUP_FORMAT:
return new TalkgroupFormatPreferenceEditor(userPreferences);
}
return null;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.preference;
@@ -28,11 +25,14 @@ package io.github.dsheirer.gui.preference;
public enum PreferenceEditorType
{
CHANNEL_EVENT("Channel Events"),
JMBE_LIBRARY("JMBE Audio Library"),
DIRECTORY("Directories"),
JMBE_LIBRARY("JMBE Audio Library"),
AUDIO_RECORD("Record"),
AUDIO_PLAYBACK("Playback"),
SOURCE_CHANNEL_MULTIPLE_FREQUENCY("Channel - Multiple Frequency"),
SOURCE_TUNER_CHANNELIZER("Tuner Channelizer"),
TALKGROUP_FORMAT("Talkgroup & Radio ID");
TALKGROUP_FORMAT("Talkgroup & Radio ID"),
DEFAULT("Default");
private String mLabel;

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.gui.preference;
@@ -29,12 +28,13 @@ import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.stage.Window;
@@ -52,25 +52,19 @@ public class PreferencesEditor extends Application
{
private final static Logger mLog = LoggerFactory.getLogger(PreferencesEditor.class);
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
private UserPreferences mUserPreferences;
private BorderPane mBorderPane;
private HBox mParentBox;
private TreeView mEditorSelectionTreeView;
private DecodeEventViewPreferenceEditor mDecodeEventViewPreferenceEditor;
private VBox mDefaultEditor;
private HBox mControlBox;
/**
* Constructs a preferences editor instance
*/
public PreferencesEditor()
{
}
private VBox mEditorAndButtonsBox;
private Node mEditor;
private HBox mButtonsBox;
public Stage getStage()
{
try
{
Window window = getBorderPane().getScene().getWindow();
Window window = getParentBox().getScene().getWindow();
if(window instanceof Stage)
{
@@ -104,7 +98,7 @@ public class PreferencesEditor extends Application
public void start(Stage stage) throws Exception
{
stage.setTitle("Preferences");
Scene scene = new Scene(getBorderPane(), 900, 500);
Scene scene = new Scene(getParentBox(), 900, 500);
stage.setScene(scene);
stage.show();
}
@@ -130,8 +124,7 @@ public class PreferencesEditor extends Application
{
TreeItem treeItem = li.next();
if(treeItem.getValue() instanceof PreferenceEditorType &&
((PreferenceEditorType)treeItem.getValue()).equals(type))
if(treeItem.getValue() instanceof PreferenceEditorType && (treeItem.getValue()).equals(type))
{
return treeItem;
}
@@ -153,31 +146,48 @@ public class PreferencesEditor extends Application
/**
* Primary layout for the editor window
*/
private BorderPane getBorderPane()
private HBox getParentBox()
{
if(mBorderPane == null)
if(mParentBox == null)
{
mBorderPane = new BorderPane();
mBorderPane.setLeft(getEditorSelectionTreeView());
mBorderPane.setCenter(getDefaultEditor());
mBorderPane.setBottom(getControlBox());
mParentBox = new HBox();
mParentBox.getChildren().add(getEditorSelectionTreeView());
HBox.setHgrow(getEditorAndButtonsBox(), Priority.ALWAYS);
mParentBox.getChildren().add(getEditorAndButtonsBox());
}
return mBorderPane;
return mParentBox;
}
private VBox getDefaultEditor()
private VBox getEditorAndButtonsBox()
{
if(mDefaultEditor == null)
if(mEditorAndButtonsBox == null)
{
mDefaultEditor = new VBox();
mDefaultEditor.setPadding(new Insets(10, 10, 10, 10));
Label label = new Label("Please select a preference ...");
mDefaultEditor.getChildren().add(label);
mEditorAndButtonsBox = new VBox();
mEditor = getDefaultEditor();
VBox.setVgrow(getDefaultEditor(), Priority.ALWAYS);
VBox.setVgrow(getButtonsBox(), Priority.NEVER);
mEditorAndButtonsBox.getChildren().addAll(getDefaultEditor(), getButtonsBox());
}
return mDefaultEditor;
return mEditorAndButtonsBox;
}
private Node getDefaultEditor()
{
Node editor = mEditors.get(PreferenceEditorType.DEFAULT);
if(editor == null)
{
VBox defaultEditor = new VBox();
defaultEditor.setPadding(new Insets(10, 10, 10, 10));
Label label = new Label("Please select a preference ...");
defaultEditor.getChildren().add(label);
mEditors.put(PreferenceEditorType.DEFAULT, defaultEditor);
editor = defaultEditor;
}
return editor;
}
/**
@@ -189,6 +199,12 @@ public class PreferencesEditor extends Application
{
TreeItem<String> treeRoot = new TreeItem<>("Root node");
TreeItem<String> audioItem = new TreeItem<>("Audio");
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_PLAYBACK));
audioItem.getChildren().add(new TreeItem(PreferenceEditorType.AUDIO_RECORD));
treeRoot.getChildren().add(audioItem);
audioItem.setExpanded(true);
TreeItem<String> decoderItem = new TreeItem<>("Decoder");
decoderItem.getChildren().add(new TreeItem(PreferenceEditorType.JMBE_LIBRARY));
treeRoot.getChildren().add(decoderItem);
@@ -217,43 +233,56 @@ public class PreferencesEditor extends Application
treeRoot.setExpanded(true);
mEditorSelectionTreeView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
mEditorSelectionTreeView.getSelectionModel().selectedItemProperty().addListener(new EditorTreeSelectionListener());
mEditorSelectionTreeView.setMinWidth(Control.USE_PREF_SIZE);
}
return mEditorSelectionTreeView;
}
/**
* Decode event view preferences editor
*/
private DecodeEventViewPreferenceEditor getDecodeEventViewPreferenceEditor()
{
if(mDecodeEventViewPreferenceEditor == null)
{
mDecodeEventViewPreferenceEditor = new DecodeEventViewPreferenceEditor(getUserPreferences());
}
return mDecodeEventViewPreferenceEditor;
}
/**
* Control box with OK button.
*/
private HBox getControlBox()
private HBox getButtonsBox()
{
if(mControlBox == null)
if(mButtonsBox == null)
{
mControlBox = new HBox();
mButtonsBox = new HBox();
mButtonsBox.setMaxWidth(Double.MAX_VALUE);
Button okButton = new Button("Ok");
okButton.setOnAction(event -> {
Stage stage = (Stage)getBorderPane().getScene().getWindow();
Stage stage = (Stage)getParentBox().getScene().getWindow();
stage.close();
});
HBox.setMargin(okButton, new Insets(5, 5, 5, 5));
mControlBox.setAlignment(Pos.CENTER_RIGHT);
mControlBox.getChildren().add(okButton);
mButtonsBox.setAlignment(Pos.CENTER_RIGHT);
mButtonsBox.getChildren().add(okButton);
}
return mControlBox;
return mButtonsBox;
}
private void setEditor(PreferenceEditorType type)
{
Node editor = mEditors.get(type);
if(editor == null)
{
if(type == PreferenceEditorType.DEFAULT)
{
editor = getDefaultEditor();
}
else
{
editor = PreferenceEditorFactory.getEditor(type, getUserPreferences());
mEditors.put(type, editor);
}
}
getEditorAndButtonsBox().getChildren().remove(mEditor);
VBox.setVgrow(editor, Priority.ALWAYS);
mEditor = editor;
getEditorAndButtonsBox().getChildren().add(0, mEditor);
}
/**
@@ -263,44 +292,22 @@ public class PreferencesEditor extends Application
*/
public class EditorTreeSelectionListener implements ChangeListener
{
private Map<PreferenceEditorType,Node> mEditors = new HashMap<>();
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue)
{
Node editor = getEditor(newValue);
BorderPane.setAlignment(editor, Pos.CENTER_LEFT);
getBorderPane().setCenter(editor);
}
private Node getEditor(Object treeNodeItem)
{
if(treeNodeItem instanceof TreeItem)
if(newValue instanceof TreeItem)
{
Object value = ((TreeItem)treeNodeItem).getValue();
Object value = ((TreeItem)newValue).getValue();
if(value instanceof PreferenceEditorType)
{
PreferenceEditorType type = (PreferenceEditorType)value;
Node editor = mEditors.get(type);
if(editor != null)
{
return editor;
}
editor = PreferenceEditorFactory.getEditor(type, getUserPreferences());
if(editor != null)
{
mEditors.put(type, editor);
return editor;
}
setEditor((PreferenceEditorType)value);
return;
}
}
return getDefaultEditor();
setEditor(PreferenceEditorType.DEFAULT);
}
}

View File

@@ -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");
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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;
}
}

View File

@@ -64,11 +64,11 @@ public class TunerPreferenceEditor extends HBox
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setVgap(10);
mEditorPane.setHgap(10);
mEditorPane.setPadding(new Insets(10, 10, 10, 10));
GridPane.setMargin(getChannelizerLabel(), new Insets(0, 10, 0, 0));
GridPane.setHalignment(getChannelizerLabel(), HPos.LEFT);
mEditorPane.add(getChannelizerLabel(), 0, 0);
GridPane.setMargin(getChannelizerTypeChoiceBox(), new Insets(2, 0, 2, 0));
mEditorPane.add(getChannelizerTypeChoiceBox(), 1, 0);
mEditorPane.add(new Separator(Orientation.HORIZONTAL), 0, 1, 2, 1);
mEditorPane.add(getPolyphaseLabel(), 0, 2, 2, 1);

View File

@@ -81,7 +81,7 @@ public class Icon implements Comparable<Icon>
{
try
{
if(mPath.startsWith("/"))
if(!mPath.startsWith("images"))
{
mImageIcon = new ImageIcon(mPath);
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.identifier;
@@ -41,7 +38,6 @@ public class IdentifierCollection
private final static Logger mLog = LoggerFactory.getLogger(IdentifierCollection.class);
protected List<Identifier> mIdentifiers = new ArrayList<>();
protected AliasListConfigurationIdentifier mAliasListConfigurationIdentifier;
private boolean mUpdated = false;
private int mTimeslot = 0;
/**
@@ -94,19 +90,6 @@ public class IdentifierCollection
mTimeslot = timeslot;
}
/**
* Indicates if this identifier collection contains an updated list of identifiers.
*/
public boolean isUpdated()
{
return mUpdated;
}
public void setUpdated(boolean updated)
{
mUpdated = updated;
}
/**
* Alias List configuration identifier containing the name of the alias list for this collection.
* @return alias list or null
@@ -319,4 +302,19 @@ public class IdentifierCollection
return null;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Identifier Collection - Timeslot:").append(mTimeslot).append("\n");
for(Identifier identifier: getIdentifiers())
{
sb.append("\t").append(identifier.toString());
sb.append("\t{").append(identifier.getIdentifierClass().name()).append("|")
.append(identifier.getForm().name()).append("|")
.append(identifier.getRole().name()).append("}");
sb.append("\t").append(identifier.getClass()).append("\n");
}
return sb.toString();
}
}

View File

@@ -98,7 +98,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
private void notifyAdd(Identifier identifier)
{
setUpdated(true);
if(mListener != null)
{
mListener.receive(new IdentifierUpdateNotification(identifier, IdentifierUpdateNotification.Operation.ADD, getTimeslot()));
@@ -110,7 +109,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
*/
private void notifyRemove(Identifier identifier)
{
setUpdated(true);
if(mListener != null)
{
mListener.receive(new IdentifierUpdateNotification(identifier,
@@ -409,8 +407,6 @@ public class MutableIdentifierCollection extends IdentifierCollection implements
public IdentifierCollection copyOf()
{
IdentifierCollection copy = new IdentifierCollection(getIdentifiers(), getTimeslot());
copy.setUpdated(isUpdated());
setUpdated(false);
return copy;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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());
}
}

View File

@@ -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