Compare commits

...

98 Commits

Author SHA1 Message Date
Dennis Sheirer
5dc12fd257 1723 Adds database folder as a user directory preference. 2024-01-27 08:32:30 -05:00
Dennis Sheirer
93f7122f57 1723 Adds call event management and deletion by age or directory size, both options in the user preferences dialog. 2024-01-27 08:08:10 -05:00
Dennis Sheirer
af2b80a066 #1723 Initial commit - audio call management feature.
-Implements Spring Boot and spring dependency injection - significant code refactoring.
-Creates database repository with Calls table
-Implements AudioManager to update database and manage audio recordings.
-Calls paging and searching
-Adds audio recording to wave file and places file in the calls database table.
-Audio spectrum view with 64 bins.
-Updates audio playback manager to process the incoming calls queue and support auto, auto-locked, replay, and mute playback modes.
-Adds channel lock/unlock and mute/unmute features.
-Adds column configuration editor to change column order & visibility and adds table state monitoring to save any user customization of the column ordering.
-Adds call table column sort ordering to the saved table state and restores the sort on application launch.
-Adds audio playback channel view options for: Complete, Standard and Minimal.  Updates user preferences Audio | Playback section to allow user to select preferred view style.
2024-01-23 17:21:56 -05:00
Denny Sheirer
42e7a49d37 #1805 DMR details tab enhancements and opcode cleanup. (#1806)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-01-23 16:38:18 -05:00
Denny Sheirer
686bddf7d5 #1799 DMR Cap+ & CapMax now track events correctly. Updated Cap+ events to include channel descriptor and frequencies. Corrected Capacity Max talker aliases to combine base and continuation alias fragments into a unified alias identifier. (#1801)
Critical: resolved a thread deadlock issue when the DMR multi-frequency channel rotation thread deadlocks against the UI thread when accessing the current channel's frequency from the frequency controller.  This thread deadlock may be the source of multiple users reporting OutOfMemory crashes when running any DMR channel with multiple control frequencies where the decoder was rolling through each frequency checking for activity.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-01-21 07:47:37 -05:00
Denny Sheirer
1c420b8eee #1787 Resolves issue with channel rotation and rest channel reallocation for Capacity Plus systems. (#1798)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-01-19 10:23:27 -05:00
Denny Sheirer
e2a0325d07 #1787 Adds CapMax talker alias continuation FLCO. Resolves issue with Con+ control channel SLCO message detection. (#1796)
#1787 Adds Capacity Max talker alias FLCO.  Fixes standard FLC Talker Alias using ISO7 format.  Updates channel details to list the tier 3 neighbors in the details.
#1787 Updates DMR capacity max event handling to better show events.  Adds support for voice channel update for both Open System and Advantage modes.  May cause issues with Cap+ or Con+ due to changes ... needs more testing.
#1787 DMR capacity max group voice update handling.

Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-01-18 11:48:25 -05:00
Denny Sheirer
9d03c151af #1787 DMR Decoder enhancements for RAS enabled systems. Adds support for CSBKO:33 and FLCO:16 (#1794)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-01-13 09:51:59 -05:00
crsawyer
3c18bfdc81 P25-P2 Encryption Synchronization ReSync (#1792)
* resync ess processor on PTT, END_PTT, HANGTIME, or IDLE

* Re-order of AbstractVoiceTimeslot. Voice2 should be processed first, as any accumulated ESS information applies to the following Voice slots

* Update P25 MBE recorders to include synchronized encryption information

---------

Co-authored-by: Sawyer, Caleb (US) - ISR <Caleb.Sawyer@L3Harris.com>
2024-01-12 09:49:49 -05:00
Denny Sheirer
d89ba65238 Merge pull request #1791 from tadscottsmith/openmhz-reconnect-error
#1791 Resolves OpenMHz Broadcaster Auto-Reconnection Issue
2024-01-12 08:36:34 -05:00
Denny Sheirer
d2cf2ee7b5 Merge pull request #1793 from DSheirer/1787-dmr-mask-manager-update
#1787 Enhance DMR CSBK CRC Calculations - Auto-Detect Alternate CRC Mask Values
2024-01-12 07:53:01 -05:00
Dennis Sheirer
7a03331955 #1787 Enhances DMR CSBK message processing by auto-detecting alternate CRC mask values employed by the DMR network. 2024-01-12 07:49:08 -05:00
tadscottsmith
7c5ef3803a Fix typo. 2024-01-10 19:06:43 -06:00
tadscottsmith
6f3e7c86e5 Fix OpenMHz Reconnection Issue. 2024-01-10 14:58:46 -06:00
Denny Sheirer
fc51de2b4f Merge pull request #1786 from DSheirer/1783-l3h-p2-talker-alias-part-2
#1783 L3H P25P2 Talker Alias Guard Against Negative Length Aliases
2024-01-10 02:48:28 -05:00
Dennis Sheirer
e1e75710cb #1783 L3H Talker Alias guard against negative length aliases 2024-01-10 02:46:22 -05:00
Denny Sheirer
87ce08e9b9 Merge pull request #1784 from DSheirer/1783-l3h-p2-message-parse-error
#1783 P25P2 L3Harris Talker Alias Index Out Of Bounds Exception
2024-01-08 03:58:17 -05:00
Dennis Sheirer
1a5c7521a5 #1783 P25P2 L3Harris Talker Alias - resolves issue with trying to parse too many bits. 2024-01-08 03:56:10 -05:00
Denny Sheirer
b54e94dfbe Merge pull request #1773 from DSheirer/1772-patch-group-streaming
1772 Patch Group Streaming Preference (Patch Group vs Individual Talkgroups)
2023-12-17 06:58:09 -05:00
Dennis Sheirer
19977d5f4d 1772 Adds preference for patch group streaming to stream patch group calls as either a)patch group, or b)each individual talkgroup or radio that forms the patch group. 2023-12-17 06:55:23 -05:00
Denny Sheirer
0492f19a95 Merge pull request #1770 from DSheirer/1769-release-0.6.0
#1769 Release version 0.6.0
2023-12-16 06:48:10 -05:00
Dennis Sheirer
098b039154 #1769 Release version 0.6.0 2023-12-16 06:44:15 -05:00
Denny Sheirer
5c6e6059f8 Merge pull request #1754 from tadscottsmith/openmhz-broadcasting
Add OpenMHz Broadcasting Support
2023-12-16 06:14:14 -05:00
Denny Sheirer
72d45543df Merge pull request #1768 from szechyjs/master
Fix osx nightly architectures
2023-12-16 05:21:06 -05:00
Jared Szechy
e96fc46315 Fix osx nightly architectures
Resolves #1766
2023-12-15 13:59:35 -05:00
Denny Sheirer
f0cfbb80dd Merge pull request #1764 from DSheirer/1760-dmr-vendor-ids
#1760 Update DMR vendor enumeration.
2023-12-14 07:50:12 -05:00
Dennis Sheirer
a199f03a60 #1760 Updates DMR vendor enumeration. 2023-12-14 07:47:45 -05:00
Denny Sheirer
526c9609ef Merge pull request #1762 from DSheirer/1761-hytera-dmr-talker-alias-and-status
#1761 DMR Hytera Tier 3 UTF-16 Talker Alias & Traffic Channel Talker Status CSBK
2023-12-14 07:13:36 -05:00
Dennis Sheirer
ec9ceeefb3 #1761 DMR decoder supports Hytera Tier 3 UTF-16 Talker Alias & new Traffic Channel Talker Status CSBKO 47. Implements a tracking feature to detect when vendors are playing games using non-standard CSBK CRC mask values by tracking when a non-standard mask value is consistently detected and applying that mask when the initial CRC check fails. 2023-12-14 07:10:30 -05:00
Denny Sheirer
b6c6505824 Merge pull request #1759 from DSheirer/1758-l3h-talker-alias-gps
#1758 P25 Phase 2 L3Harris Talker Alias and GPS Report.
2023-12-10 09:52:48 -05:00
Dennis Sheirer
93c61c48f9 #1758 Adds P25 Phase 2 L3Harris talker alias and GPS report. 2023-12-10 09:45:49 -05:00
Denny Sheirer
9b89e80bb5 Merge pull request #1756 from DSheirer/1753-sdrplay-remove-unsupported-ifagc-modes
#1753 SDRPlay - Remove Deprecated AGC Modes
2023-12-01 03:43:16 -05:00
Dennis Sheirer
e0ecaa6e08 #1753 Changes tuner editors to use toggle button for IF AGC mode and updates enum lookup to always return either ENABLE or DISABLE. Deprecates 3x enum values as no longer supported. 2023-12-01 03:39:57 -05:00
Denny Sheirer
a7c89b5d1a Merge pull request #1755 from DSheirer/1746-dmr-simplex-repeater
#1746 DMR Decoder - Simplex Repeater and DCDM Calls
2023-12-01 03:02:13 -05:00
Dennis Sheirer
834846d339 #1746 DMR Decoder now supports simplex repeater and DMDC modes of operation. 2023-12-01 02:58:27 -05:00
tadscottsmith
45c8bb73fa Authorization features. 2023-11-29 14:40:45 -06:00
Denny Sheirer
d2580f8a99 Merge pull request #1751 from DSheirer/1188-fully-qualified-radio-identiifer-aliasing
#1188 APCO25 (ISSI) Fully Qualified Radio & Fully Qualified Talkgroup Aliasing
2023-11-29 02:34:43 -05:00
Dennis Sheirer
804c2e6359 #1188 Add support for P25 fully qualified radio and talkgroup identifiers to aliases and alias lists. Resolve issue with ESN alias lookups. 2023-11-28 04:32:11 -05:00
Denny Sheirer
3f2ec612d5 Merge pull request #1745 from DSheirer/1743-cpu-load-info-not-available
#1743 CPU Status Indicator & Show/Hide Status Panel & Now Playing Channel Detail Tabs
2023-11-18 03:29:39 -05:00
Dennis Sheirer
8bead998f5 #1743 Resolves CPU status indicator strobing and adds control to show/hide status panel and now playing channel details tabs. 2023-11-18 03:26:21 -05:00
tadscottsmith
2ecdaba183 Initial commit. (#4) 2023-11-15 17:23:09 -06:00
Denny Sheirer
82cb10a03d Merge pull request #1738 from hans109h/patch-1
Update JavaFxWindowManager.java for #1619
2023-11-15 05:01:16 -05:00
Denny Sheirer
8717ce38eb Merge pull request #1741 from DSheirer/1735-radio-ref-api-invalid-county-id-temp-site
#1735 Radio Reference API - Temporary Site With County ID 99999 - Unknown Location
2023-11-15 04:55:02 -05:00
sheirerd
5858a85a50 #1735 Adds support for county ID with sentinel value of 99999 to indicate county unknown for sites of a system. 2023-11-15 04:51:07 -05:00
hans109h
65d16da997 Update JavaFxWindowManager.java for #1619
Fixes #1619
2023-11-14 11:10:16 -06:00
Denny Sheirer
b1b5ae5fd2 Merge pull request #1734 from DSheirer/1733-management-factory-module-missing
#1733 Java Module Missing (java.management)
2023-11-14 06:07:11 -05:00
sheirerd
b5ec6d8877 #1733 Updates gradle build script to explicitly include the java.management module. 2023-11-14 06:04:36 -05:00
Denny Sheirer
8599aebed9 Merge pull request #1730 from CrimeIsDown/rdio-scanner-http-fix
Fix Rdio Scanner API call to comply with HTTP standards
2023-11-12 07:36:57 -05:00
Denny Sheirer
2b9e753033 Merge pull request #1732 from DSheirer/1731-application-status-bar
#1731 Application Status Bar (cpu/memory/disk space).
2023-11-12 07:21:43 -05:00
Dennis Sheirer
dba50744a9 #1731 Adds application status bar with progress indicators for CPU, memory and disk space (event logs / recordings) usage. Updates user preferences to include threshold settings in the directories editor for max size for event logs and recordings. 2023-11-12 07:19:10 -05:00
Eric Tendian
490569e023 Fix Rdio Scanner API call to comply with HTTP standards 2023-11-12 00:43:48 -06:00
Denny Sheirer
e79f2af759 Merge pull request #1728 from DSheirer/1619-javafx-windows-reappear-iconified
#1619 JavaFX Windows Restore to Previous Size/Location On View Request
2023-11-11 06:47:27 -05:00
Dennis Sheirer
ec7ee1ce91 #1619 JavaFX windows restore to previous size on view. 2023-11-11 06:45:23 -05:00
Denny Sheirer
672207c27c Merge pull request #1727 from DSheirer/1726-application-root-dir-on-startup
#1726 Don't Auto-Create App Root In User Home Directory Once Changed
2023-11-11 06:28:40 -05:00
Dennis Sheirer
a1a6b3e988 #1726 On startup, application no longer auto-creates SDRTrunk folder and system properties in user's home directory when user has changed the application root in the user preferences. 2023-11-11 06:17:04 -05:00
Denny Sheirer
2347135614 Merge pull request #1725 from DSheirer/1554-view-logs-file-explorer
#1554 App View Menu Cleanup & File Explorer Location
2023-11-11 05:33:13 -05:00
Dennis Sheirer
93438ddd97 #1554 Updates application view menu to show user-selected file locations. Enhances menu items with icons. Changes icon for playlist editor tab. 2023-11-11 05:28:36 -05:00
Eric Tendian
ae31c41643 #1635 Add a special county agency to show all county freqs without an agency (#1724)
#1635 Playlist Editor Add a special county agency to show all county freqs without an agency.
Merge pull request #1724
2023-11-11 04:39:17 -05:00
Denny Sheirer
16aed8741d Merge pull request #1721 from DSheirer/1720-version-0.6.0-beta-5
#1720 Updates version to 0.6.0 Beta 5
2023-11-10 05:43:31 -05:00
Dennis Sheirer
e878ca0c7f #1720 Updates version to 0.6.0 Beta 5 2023-11-10 05:42:41 -05:00
Denny Sheirer
ca61dd2543 Merge pull request #1719 from DSheirer/1619-java-fx-windows-open-minimized
#1619 JavaFX Windows Show Minimized
2023-11-10 05:34:09 -05:00
Dennis Sheirer
6187253589 #1619 Resolves issue where JavaFX window size and placement might show minimized after application is started and an initial JavaFX window is opened and then closed, where it impacted subsequent JavaFX window openings. Corrects issue where JavaFX stage monitors were not being registered correctly, so that they could be deregistered prior to application shutdown. 2023-11-10 05:31:05 -05:00
Denny Sheirer
b51c80742f Merge pull request #1717 from DSheirer/1681-audio-recordings-incorrect-system-and-site
#1681 Audio Recorder Mixing Up System and Site When Decoding Multiple Systems
2023-11-10 04:51:54 -05:00
Dennis Sheirer
15352b9216 #1681 Resolves issue where audio recordings have incorrect system and site labels in metadata. Adds synchronized to channel processing manager's start channel request method to ensure that each channel is configured and started sequentially to avoid chance that simultaneous traffic channel grants occurring across two separate systems get inter-mingled on configuration and startup. 2023-11-10 04:34:45 -05:00
Denny Sheirer
a848b21ae8 Merge pull request #1716 from DSheirer/1715-rtl-tuner-v3-v4-mixed-tuning-issue
#1715 RTL2832 Tuner Issue When Using V4 Dongle With Other R820T Tuners
2023-11-10 03:49:53 -05:00
Dennis Sheirer
279aca7287 #1715 Resolves issue of using both legacy and new V4 RTL2832 dongles at same time causes errant tuning behavior on the first tuner that gets initialized. Removed static qualifier from VCO reference value so that each tuner is allowed to initialize correctly. 2023-11-10 03:47:01 -05:00
Denny Sheirer
88879c956a Merge pull request #1714 from DSheirer/732-p25p2-missing-from-identifier-call-event
#732 P25 Phase 2 - FROM ID Updated For Call Event Logging
2023-11-09 04:51:01 -05:00
Dennis Sheirer
729b7e27a8 #732 Resolves P25P2 call events sometimes missing FROM radio ID even though the ID was decoded. 2023-11-09 04:48:42 -05:00
Denny Sheirer
feaed168ae Merge pull request #1713 from DSheirer/967-bulk-alias-editor-set-recordable
#967 Multi-Alias Editor Supports Changing Record Audio Status
2023-11-09 04:34:28 -05:00
Dennis Sheirer
50edb18db3 967 Updates bulk alias editor to support editing alias recordable status. 2023-11-09 04:30:25 -05:00
Denny Sheirer
7302e2806d Merge pull request #1712 from DSheirer/968-alias-editor-clear-overlap-state
#968 Playlist Alias Editor - Clear Overlap Error Warning Once Resolved
2023-11-09 04:15:17 -05:00
Dennis Sheirer
8845306b4a #968 Resolves issue with clearing alias ID overlap state when the user edits the alias to correct the overlap. 2023-11-09 04:12:46 -05:00
Denny Sheirer
09e19e1486 Merge pull request #1708 from tadscottsmith/rdio-scanner-filename
Explicitly send filename to Rdio-Scanner.
2023-11-07 04:12:14 -05:00
Denny Sheirer
0f5478925c Update README.md 2023-11-07 04:01:40 -05:00
Denny Sheirer
09dce0b0d1 Merge pull request #1710 from DSheirer/1709-nightly-build-timestamp-info
#1709 Nightly Release Build Timestamp Info
2023-11-07 03:32:42 -05:00
Dennis Sheirer
ad98b1cb5e #1709 Adds build timestamp info to nightly releases. 2023-11-07 03:31:31 -05:00
tadscottsmith
8effd97749 Explicitly send filename to Rdio-Scanner. 2023-11-06 10:54:41 -06:00
Denny Sheirer
9dbed72c32 Merge pull request #1707 from DSheirer/1706-nightly-release
#1706 Github Action For Nightly Releases
2023-11-06 05:06:19 -05:00
Dennis Sheirer
b4dd3692b8 #1706 Github action to create nightly application releases. 2023-11-06 05:05:40 -05:00
Denny Sheirer
030cbb856c Update README.md 2023-11-06 04:36:59 -05:00
Denny Sheirer
511101e9b7 Merge pull request #1705 from DSheirer/1704-harris-patch-group-formatting
#1704 Patch Group Formatting
2023-11-06 02:52:49 -05:00
Dennis Sheirer
60dd2b2df7 #1704 Updates patch group format for compatibility. 2023-11-06 02:50:46 -05:00
Denny Sheirer
218dc94e02 Merge pull request #1701 from DSheirer/1644-sdrplay-api-3-12
#1644 SDRPlay API Version 3.12 For Mac OSX
2023-11-05 07:22:04 -05:00
Dennis Sheirer
2440f578b1 #1644 Update SDRPlay API for version 3.12 compatability 2023-11-05 07:20:29 -05:00
Denny Sheirer
09c87237cb Merge pull request #1700 from DSheirer/1651-gradle-build-version-naming
#1651 Version 0.6.0 Beta 4 and Version Naming Conventions
2023-11-05 07:10:00 -05:00
Dennis Sheirer
3d974ddd91 #1651 Increment version to 0.6.0 Beta 4 and add version naming conventions comments. 2023-11-05 07:08:18 -05:00
Denny Sheirer
1489c970ba Merge pull request #1699 from DSheirer/1653-channel-edit-with-filtering
#1653 Playlist Channel Edits With Applied Filter Produces Error
2023-11-05 06:58:36 -05:00
Dennis Sheirer
9e0afca3d1 #1653 Playlist channel edits with applied filter doesn't produce error 2023-11-05 06:56:49 -05:00
Denny Sheirer
99eda4013e Merge pull request #1698 from DSheirer/1654-playlist-editor-channel-frequency-sort
#1654 Playlist Channel Editor Frequency Column Sort
2023-11-05 06:32:23 -05:00
Dennis Sheirer
a62f81f890 #1654 Playlist channel editor sorts by frequency numerically (vs lexicographically) 2023-11-05 06:29:23 -05:00
Denny Sheirer
66f0731731 Merge pull request #1697 from DSheirer/1665-refresh-broadcastify-streams
#1665 Refresh Broadcastify Streams In Playlist Editor
2023-11-05 06:00:48 -05:00
Dennis Sheirer
dc239801c6 #1665 Add option to refresh available streams from broadcastify in playlist editor's streaming tab. 2023-11-05 05:58:32 -05:00
Denny Sheirer
d4ba0988bc Merge pull request #1696 from DSheirer/1673-playlist-editor-broadcastify-system-id
#1673 Broadcastify Call Streaming Config Editor's default value of 0 …
2023-11-05 05:24:03 -05:00
Dennis Sheirer
ad0f953b2a #1673 Broadcastify Call Streaming Config Editor's default value of 0 causes field to be uneditable. 2023-11-05 05:20:15 -05:00
Denny Sheirer
e1bc9eabd6 Merge pull request #1695 from DSheirer/1694-sdrplay-rsp-tuner-memory-leaks
#1694 SDRPlay RSP Memory Leak & Audio Playback Delays
2023-11-05 04:26:48 -05:00
Dennis Sheirer
d3dc1a8667 #1694 Resolves memory leak in SDRPlay RSP tuner implementation. Sample stream and event callback functions were using a global arena for memory allocation where memory was not being de-allocated. Changed to on-demand arena creation and deterministic memory de-allocation. Also, when using a reduced sample rate from the RSP tuner, the dispatcher was not keeping up with the inbound sample buffer rate, resulting in a buildup of RSP native sample buffers over time and causing growing audio delays with broken/stilted audio during playback. 2023-11-05 04:24:19 -05:00
Denny Sheirer
bf278120de Merge pull request #1692 from DSheirer/DSheirer-patch-1
Update README.md
2023-11-04 07:41:29 -04:00
Denny Sheirer
2f52f6e009 Update README.md 2023-11-04 07:41:12 -04:00
321 changed files with 19391 additions and 5235 deletions

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up JDK 20
uses: actions/setup-java@v3
with:

47
.github/workflows/nightly.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
# Create nightly release for Linux, Mac and Windows using the Linux runner.
name: Nightly Release
on:
push:
branches: [ master ]
# Don't run nightly build for pull requests - uncomment to test changes to this action within scope of the PR review
# pull_request:
# branches: [ master ]
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 20
uses: actions/setup-java@v3
with:
java-version: '20'
distribution: 'liberica'
java-package: 'jdk+fx'
- name: Build Linux and Mac releases with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: runtimeZipOthers -PprojectVersion=nightly
- name: Build Windows release with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: runtimeZipWindows -PprojectVersion=nightly
- name: Create Build Info file
run: |
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
- name: Add build artifacts to nightly release
uses: pyTooling/Actions/releaser@main
with:
tag: nightly
rm: false
token: ${{ secrets.GITHUB_TOKEN }}
files: |
build/image/*.zip
build/image/build_info.txt

2
.idea/misc.xml generated
View File

@@ -4,7 +4,7 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_20" default="true" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_20_PREVIEW" project-jdk-name="liberica-20" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,8 +1,10 @@
![Gradle Build](https://github.com/dsheirer/sdrtrunk/actions/workflows/gradle.yml/badge.svg)
![Nightly Release](https://github.com/dsheirer/sdrtrunk/actions/workflows/nightly.yml/badge.svg)
# sdrtrunk
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
* [Help/Wiki Home Page](https://github.com/DSheirer/sdrtrunk/wiki)
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/Getting-Started)
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
@@ -18,6 +20,13 @@ All release versions of sdrtrunk are available from the [releases](https://githu
* **(beta)** These versions are currently being tested for bugs and functionality prior to final release.
* **(final)** These versions have been tested and are the current release version.
## Download Nightly Software Build
The [nightly](https://github.com/DSheirer/sdrtrunk/releases/tag/nightly) release contains current builds of the software
for all supported operating systems. This version of the software may contain bugs and may not run correctly. However,
it let's you preview the most recent changes and fixes before the next software release. **Always backup your
playlist(s) before you use the nightly builds.** Note: the nightly release is updated each time code changes are
committed to the code base, so it's not really 'nightly' as much as it is 'current'.
## Minimum System Requirements
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **CPU:** 4-core

View File

@@ -23,8 +23,8 @@ import java.text.SimpleDateFormat
* Instructions for building/compiling the sdrtrunk application.
*
* Prerequisites:
* Install and configure an OpenJDK version 19+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
* - Optional: install and configure Gradle 7.6+
* Install and configure an OpenJDK version 20 that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
* - Optional: install and configure Gradle 8.2+
*
* Scenario 1: run the application via gradle command line from the source code root directory:
* command: ./gradlew run
@@ -46,6 +46,8 @@ plugins {
id 'java'
id 'idea'
id 'org.beryx.runtime' version '1.12.7'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
}
repositories {
@@ -53,8 +55,8 @@ repositories {
maven { url "https://jitpack.io" }
}
//Note: this should match the GitLab release tag.
version = '0.6.0-beta-3'
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 20 is required for this version of the Project Panama preview/incubator feature
java {
@@ -92,6 +94,7 @@ dependencies {
implementation 'ch.qos.logback:logback-core:1.4.5'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
implementation 'com.fazecast:jSerialComm:2.9.3'
implementation 'com.github.jiconfont:jiconfont-elusive:2.0.3'
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'
@@ -103,6 +106,7 @@ dependencies {
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.11.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
@@ -110,15 +114,18 @@ dependencies {
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-csv:1.9.0'
implementation 'org.apache.derby:derby'
implementation 'org.apache.derby:derbytools'
implementation 'org.apache.mina:mina-core:2.2.1'
implementation 'org.apache.mina:mina-http:2.2.1'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'org.rauschig:jarchivelib:1.2.0'
implementation 'org.slf4j:slf4j-api:2.0.5'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
@@ -211,8 +218,8 @@ def jdk_download_base = "https://download.bell-sw.com/java/20.0.1+10/bellsoft-jd
def jdk_download_suffix = "-full.tar.gz"
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
def jdk_download_osx_x86_64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
def jdk_download_osx_aarch64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
def jdk_download_osx_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
def jdk_download_osx_aarch64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
/**
@@ -229,7 +236,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
//jdk.accessibility is used with assistive technologies like screen readers
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility')
//java.management for JVM resource monitoring
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management', 'java.instrument')
//Use auto-detected modules and 'add' any specified modules.
rt.extension.additive.set(true)
@@ -340,7 +348,7 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
jdkHome = jdkDownload(jdk_download_osx_aarch64)
}
}
//Linux x86-64
//OSX x86-64
if(file(jdk_osx_x86_64).exists()) {
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
}
@@ -352,4 +360,3 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
configure(rt, jvmArgsLinux);
}

31
gradle.properties Normal file
View File

@@ -0,0 +1,31 @@
#
# ******************************************************************************
# Copyright (C) 2014-2024 Dennis Sheirer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
# *****************************************************************************
#
# Version naming conventions:
# Alpha: maj.min.patch-alpha-build
# Beta: maj.min.patch-beta-build
# Release: maj.min.patch
#
# Example: 0.6.0-beta-5
#
# Note: this is so that the file names for the resultant release build products match the GitLab release asset
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
projectVersion=0.7.0-alpha-1

View File

@@ -30,11 +30,13 @@ import io.github.dsheirer.alias.id.legacy.mobileID.Min;
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
import io.github.dsheirer.alias.id.radio.Radio;
import io.github.dsheirer.alias.id.radio.RadioRange;
import io.github.dsheirer.alias.id.record.Record;
import io.github.dsheirer.alias.id.status.UnitStatusID;
import io.github.dsheirer.alias.id.status.UserStatusID;
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.alias.id.tone.TonesID;
@@ -77,6 +79,18 @@ public class AliasFactory
Min copyMin = new Min();
copyMin.setMin(originalMin.getMin());
return copyMin;
case P25_FULLY_QUALIFIED_RADIO_ID:
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),
originalP25.getSystem(), originalP25.getValue());
copyP25.setOverlap(originalP25.overlapProperty().get());
return copyP25;
case P25_FULLY_QUALIFIED_TALKGROUP:
P25FullyQualifiedTalkgroup originalFqt = (P25FullyQualifiedTalkgroup) id;
P25FullyQualifiedTalkgroup copyFqt = new P25FullyQualifiedTalkgroup(originalFqt.getWacn(),
originalFqt.getSystem(), originalFqt.getValue());
copyFqt.setOverlap(originalFqt.overlapProperty().get());
return copyFqt;
case PRIORITY:
Priority originalPriority = (Priority)id;
Priority copyPriority = new Priority();
@@ -184,6 +198,7 @@ public class AliasFactory
copy.setGroup(original.getGroup());
copy.setColor(original.getColor());
copy.setIconName(original.getIconName());
copy.setRecordable(original.isRecordable());
return copy;
}

View File

@@ -24,10 +24,12 @@ import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.alias.id.dcs.Dcs;
import io.github.dsheirer.alias.id.esn.Esn;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
import io.github.dsheirer.alias.id.radio.Radio;
import io.github.dsheirer.alias.id.radio.RadioRange;
import io.github.dsheirer.alias.id.status.UnitStatusID;
import io.github.dsheirer.alias.id.status.UserStatusID;
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.alias.id.tone.TonesID;
@@ -37,9 +39,11 @@ import io.github.dsheirer.identifier.dcs.DCSIdentifier;
import io.github.dsheirer.identifier.esn.ESNIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
import io.github.dsheirer.identifier.status.UserStatusIdentifier;
import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.identifier.tone.ToneIdentifier;
import io.github.dsheirer.identifier.tone.ToneSequence;
@@ -155,6 +159,32 @@ public class AliasList
talkgroupRangeAliasList.add(talkgroupRange, alias);
break;
case P25_FULLY_QUALIFIED_RADIO_ID:
P25FullyQualifiedRadio qualifiedRadio = (P25FullyQualifiedRadio) id;
RadioAliasList p25RadioAliasList = mRadioProtocolMap.get(qualifiedRadio.getProtocol());
if(p25RadioAliasList == null)
{
p25RadioAliasList = new RadioAliasList();
mRadioProtocolMap.put(qualifiedRadio.getProtocol(), p25RadioAliasList);
}
p25RadioAliasList.add(qualifiedRadio, alias);
break;
case P25_FULLY_QUALIFIED_TALKGROUP:
P25FullyQualifiedTalkgroup qualifiedTalkgroup = (P25FullyQualifiedTalkgroup) id;
TalkgroupAliasList p25TalkgroupAliasList = mTalkgroupProtocolMap.get(qualifiedTalkgroup.getProtocol());
if(p25TalkgroupAliasList == null)
{
p25TalkgroupAliasList = new TalkgroupAliasList();
mTalkgroupProtocolMap.put(qualifiedTalkgroup.getProtocol(), p25TalkgroupAliasList);
}
p25TalkgroupAliasList.add(qualifiedTalkgroup, alias);
break;
case RADIO_ID:
Radio radio = (Radio)id;
@@ -444,7 +474,7 @@ public class AliasList
return toList(radioAliasList.getAlias(radio));
}
break;
case DCS:
case ESN:
if(identifier instanceof ESNIdentifier)
{
return toList(getESNAlias(((ESNIdentifier)identifier).getValue()));
@@ -623,6 +653,7 @@ public class AliasList
*/
public class TalkgroupAliasList
{
private Map<String,Alias> mFullyQualifiedTalkgroupAliasMap = new HashMap<>();
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
@@ -632,6 +663,11 @@ public class AliasList
public Alias getAlias(TalkgroupIdentifier identifier)
{
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
{
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
}
int value = identifier.getValue();
Alias mapValue = mTalkgroupAliasMap.get(value);
@@ -653,26 +689,55 @@ public class AliasList
public void add(Talkgroup talkgroup, Alias alias)
{
//Detect talkgroup collisions and set overlap flag for both
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
if(talkgroup instanceof P25FullyQualifiedTalkgroup fqt)
{
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
if(!existing.equals(alias))
//Detect collisions
if(mFullyQualifiedTalkgroupAliasMap.containsKey(fqt.getHashKey()))
{
talkgroup.setOverlap(true);
Alias existing = mFullyQualifiedTalkgroupAliasMap.get(fqt.getHashKey());
for(AliasID aliasID: existing.getAliasIdentifiers())
if(!existing.equals(alias))
{
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
fqt.setOverlap(true);
for(AliasID aliasID: existing.getAliasIdentifiers())
{
aliasID.setOverlap(true);
if(aliasID instanceof P25FullyQualifiedTalkgroup existingFqt &&
existingFqt.getHashKey().contentEquals(fqt.getHashKey()))
{
aliasID.setOverlap(true);
}
}
}
}
else
{
mFullyQualifiedTalkgroupAliasMap.put(fqt.getHashKey(), alias);
}
}
else
{
//Detect talkgroup collisions and set overlap flag for both
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
{
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
if(!existing.equals(alias))
{
talkgroup.setOverlap(true);
for(AliasID aliasID: existing.getAliasIdentifiers())
{
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
{
aliasID.setOverlap(true);
}
}
}
}
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
}
}
public void add(TalkgroupRange talkgroupRange, Alias alias)
@@ -705,6 +770,7 @@ public class AliasList
*/
public class RadioAliasList
{
private Map<String,Alias> mFullyQualifiedRadioAliasMap = new HashMap<>();
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
@@ -714,6 +780,11 @@ public class AliasList
public Alias getAlias(RadioIdentifier identifier)
{
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
{
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
}
int value = identifier.getValue();
Alias mapValue = mRadioAliasMap.get(value);
@@ -735,26 +806,55 @@ public class AliasList
public void add(Radio radio, Alias alias)
{
//Detect collisions
if(mRadioAliasMap.containsKey(radio.getValue()))
if(radio instanceof P25FullyQualifiedRadio fqr)
{
Alias existing = mRadioAliasMap.get(radio.getValue());
if(!existing.equals(alias))
//Detect collisions
if(mFullyQualifiedRadioAliasMap.containsKey(fqr.getHashKey()))
{
radio.setOverlap(true);
Alias existing = mFullyQualifiedRadioAliasMap.get(fqr.getHashKey());
for(AliasID aliasID: existing.getAliasIdentifiers())
if(!existing.equals(alias))
{
if(aliasID instanceof Radio && ((Radio)aliasID).getValue() == radio.getValue())
fqr.setOverlap(true);
for(AliasID aliasID: existing.getAliasIdentifiers())
{
aliasID.setOverlap(true);
if(aliasID instanceof P25FullyQualifiedRadio existingFqr &&
existingFqr.getHashKey().contentEquals(fqr.getHashKey()))
{
aliasID.setOverlap(true);
}
}
}
}
else
{
mFullyQualifiedRadioAliasMap.put(fqr.getHashKey(), alias);
}
}
else
{
//Detect collisions
if(mRadioAliasMap.containsKey(radio.getValue()))
{
Alias existing = mRadioAliasMap.get(radio.getValue());
mRadioAliasMap.put(radio.getValue(), alias);
if(!existing.equals(alias))
{
radio.setOverlap(true);
for(AliasID aliasID: existing.getAliasIdentifiers())
{
if(aliasID instanceof Radio existingRadio && (existingRadio.getValue() == radio.getValue()))
{
aliasID.setOverlap(true);
}
}
}
}
mRadioAliasMap.put(radio.getValue(), alias);
}
}
public void add(RadioRange radioRange, Alias alias)

View File

@@ -33,12 +33,14 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Alias Model contains all aliases and is responsible for creation and management of alias lists. Alias lists are a
* set of aliases that all share a common alias list name and can be attached to a decoding channel for aliasing
* identifiers produced by channel decoder(s).
*/
@Component("aliasModel")
public class AliasModel
{
private final static Logger mLog = LoggerFactory.getLogger(AliasModel.class);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -24,19 +24,19 @@ import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.action.AliasActionType;
import io.github.dsheirer.alias.action.RecurringAction;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.gui.preference.playback.ToneFrequency;
import io.github.dsheirer.gui.preference.playback.ToneUtil;
import io.github.dsheirer.gui.preference.playback.ToneVolume;
import io.github.dsheirer.gui.preference.audio.playback.ToneFrequency;
import io.github.dsheirer.gui.preference.audio.playback.ToneUtil;
import io.github.dsheirer.gui.preference.audio.playback.ToneVolume;
import io.github.dsheirer.message.IMessage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
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;
public class BeepAction extends RecurringAction
{

View File

@@ -35,11 +35,13 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
import io.github.dsheirer.alias.id.radio.Radio;
import io.github.dsheirer.alias.id.radio.RadioRange;
import io.github.dsheirer.alias.id.record.Record;
import io.github.dsheirer.alias.id.status.UnitStatusID;
import io.github.dsheirer.alias.id.status.UserStatusID;
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.alias.id.tone.TonesID;
@@ -61,6 +63,8 @@ import javafx.util.Callback;
@JsonSubTypes.Type(value = Min.class, name = "min"),
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),

View File

@@ -29,6 +29,8 @@ public enum AliasIDType
LOJACK("LoJack"),
LTR_NET_UID("LTR-Net UID"),
MIN("Passport MIN"),
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
PRIORITY("Audio Priority"),
RADIO_ID("Radio ID"),
RADIO_ID_RANGE("Radio ID Range"),
@@ -56,7 +58,7 @@ public enum AliasIDType
//Values used by the View-By alias editor
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
UNIT_STATUS, STATUS, TONES);
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
public String toString()
{

View File

@@ -0,0 +1,127 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.radio;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.protocol.Protocol;
/**
* Fully qualified radio. Note: this is only for P25.
*/
public class P25FullyQualifiedRadio extends Radio
{
private int mWacn;
private int mSystem;
public P25FullyQualifiedRadio()
{
//No-arg JAXB constructor
}
/**
* Constructs an instance
* @param wacn for the radio
* @param system for the radio
* @param id for the radio
*/
public P25FullyQualifiedRadio(int wacn, int system, int id)
{
super(Protocol.APCO25, id);
mWacn = wacn;
mSystem = system;
}
@Override
public AliasIDType getType()
{
return AliasIDType.P25_FULLY_QUALIFIED_RADIO_ID;
}
/**
* WACN for this radio
* @return wacn
*/
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
public int getWacn()
{
return mWacn;
}
/**
* Sets the WACN for this radio
* @param wacn for this radio
*/
public void setWacn(int wacn)
{
mWacn = wacn;
}
/**
* System for this radio
* @return system
*/
@JacksonXmlProperty(isAttribute = true, localName = "system")
public int getSystem()
{
return mSystem;
}
/**
* Sets the system for this radio
* @param system for this radio
*/
public void setSystem(int system)
{
mSystem = system;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Fully Qualified Radio ID:").append(getWacn());
sb.append(".").append(getSystem());
sb.append(".").append(getValue());
sb.append(" Protocol:").append((getProtocol()));
if(!isValid())
{
sb.append(" **NOT VALID**");
}
return sb.toString();
}
/**
* Hashable string key for use in a lookup map
*/
@JsonIgnore
public String getHashKey()
{
StringBuilder sb = new StringBuilder();
sb.append(getWacn());
sb.append(".").append(getSystem());
sb.append(".").append(getValue());
return sb.toString();
}
}

View File

@@ -0,0 +1,128 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.talkgroup;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.protocol.Protocol;
/**
* Fully qualified talkgroup. Note: this is only for P25.
*/
public class P25FullyQualifiedTalkgroup extends Talkgroup
{
private int mWacn;
private int mSystem;
public P25FullyQualifiedTalkgroup()
{
//No-arg JAXB constructor
}
/**
* Constructs an instance
* @param wacn for the radio
* @param system for the radio
* @param talkgroup value
*/
public P25FullyQualifiedTalkgroup(int wacn, int system, int talkgroup)
{
super(Protocol.APCO25, talkgroup);
mWacn = wacn;
mSystem = system;
}
@Override
public AliasIDType getType()
{
return AliasIDType.P25_FULLY_QUALIFIED_TALKGROUP;
}
/**
* WACN for this radio
* @return wacn
*/
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
public int getWacn()
{
return mWacn;
}
/**
* Sets the WACN for this radio
* @param wacn for this radio
*/
public void setWacn(int wacn)
{
mWacn = wacn;
}
/**
* System for this radio
* @return system
*/
@JacksonXmlProperty(isAttribute = true, localName = "system")
public int getSystem()
{
return mSystem;
}
/**
* Sets the system for this radio
* @param system for this radio
*/
public void setSystem(int system)
{
mSystem = system;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Fully Qualified Radio ID:").append(getWacn());
sb.append(".").append(getSystem());
sb.append(".").append(getValue());
sb.append(" Protocol:").append((getProtocol()));
if(!isValid())
{
sb.append(" **NOT VALID**");
}
return sb.toString();
}
/**
* Hashable string key for use in a lookup map
* @return
*/
@JsonIgnore
public String getHashKey()
{
StringBuilder sb = new StringBuilder();
sb.append(getWacn());
sb.append(".").append(getSystem());
sb.append(".").append(getValue());
return sb.toString();
}
}

View File

@@ -20,6 +20,7 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.identifier.IdentifierUpdateListener;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
@@ -89,7 +90,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
{
mAudioSegment.completeProperty().set(true);
mIdentifierUpdateNotificationBroadcaster.removeListener(mAudioSegment);
mAudioSegment.decrementConsumerCount();
mAudioSegment.removeLease(getClass().toString());
mAudioSegment = null;
}
}
@@ -112,7 +113,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
if(mAudioSegment == null)
{
mAudioSegment = new AudioSegment(mAliasList, getTimeslot());
mAudioSegment.incrementConsumerCount();
mAudioSegment.addLease(getClass().toString());
mAudioSegment.addIdentifiers(mIdentifierCollection.getIdentifiers());
mIdentifierUpdateNotificationBroadcaster.addListener(mAudioSegment);
@@ -123,7 +124,6 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
if(mAudioSegmentListener != null)
{
mAudioSegment.incrementConsumerCount();
mAudioSegmentListener.receive(mAudioSegment);
}
@@ -136,26 +136,33 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
public void addAudio(float[] audioBuffer)
{
AudioSegment audioSegment = getAudioSegment();
if(audioBuffer != null)
{
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 >= mMaxSegmentAudioSampleLength)
{
AudioSegment previous = getAudioSegment();
closeAudioSegment();
audioSegment = getAudioSegment();
audioSegment.linkTo(previous);
}
//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 >= mMaxSegmentAudioSampleLength)
{
AudioSegment previous = getAudioSegment();
closeAudioSegment();
audioSegment = getAudioSegment();
audioSegment.linkTo(previous);
}
try
{
audioSegment.addAudio(audioBuffer);
mAudioSampleCount += audioBuffer.length;
try
{
audioSegment.addAudio(audioBuffer);
mAudioSampleCount += audioBuffer.length;
}
catch(Exception e)
{
closeAudioSegment();
}
}
catch(Exception e)
else
{
closeAudioSegment();
mLog.info("Attempt to add null audio from " + getClass());
}
}

View File

@@ -0,0 +1,503 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio;
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
import io.github.dsheirer.audio.call.ActiveAudioSegment;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.audio.call.Call;
import io.github.dsheirer.audio.call.CallRepository;
import io.github.dsheirer.audio.call.ICallEventListener;
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
import io.github.dsheirer.audio.playbackfx.AudioPlaybackController;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.retention.AgeUnits;
import io.github.dsheirer.preference.retention.RetentionPolicy;
import io.github.dsheirer.preference.retention.RetentionPreference;
import io.github.dsheirer.record.AudioRecordingManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Component;
/**
* Manages handling and distribution of audio segments and calls across the application.
*/
@Component("audioManager")
public class AudioManager implements Listener<AudioSegment>
{
private static final Logger LOGGER = LoggerFactory.getLogger(AudioManager.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOGGER);
private static final long GIGABYTES = 1024l * 1024l * 1024l;
@Resource
private AudioPlaybackManager mAudioPlaybackManager;
@Resource
private AudioPlaybackController mAudioPlaybackController;
@Resource
private AudioRecordingManager mAudioRecordingManager;
@Resource
private AudioStreamingManager mAudioStreamingManager;
@Resource
private DuplicateCallDetector mDuplicateCallDetector;
@Resource
private UserPreferences mUserPreferences;
private CallRepository mCallRepository;
private List<ICallEventListener> mCallListeners = new CopyOnWriteArrayList<>();
private ActiveAudioSegmentProcessor mAudioSegmentProcessor = new ActiveAudioSegmentProcessor();
private CallAgeOffProcessor mCallAgeOffProcessor = new CallAgeOffProcessor();
private ScheduledFuture<?> mAudioSegmentProcessorFuture;
private ScheduledFuture<?> mCallRetentionProcessorFuture;
/**
* Constructs an instance
*/
@Autowired
public AudioManager(CallRepository callRepository)
{
mCallRepository = callRepository;
}
@PostConstruct
public void postConstruct()
{
LOGGER.info("Post constructing ......");
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mAudioSegmentProcessor, 100,
500, TimeUnit.MILLISECONDS);
//Delay first run until 1-minute after startup.
mCallRetentionProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(mCallAgeOffProcessor,
30, 30, TimeUnit.SECONDS);
//Register call event listeners
add(mAudioRecordingManager);
add(mAudioPlaybackController);
add(mAudioStreamingManager);
}
@PreDestroy
public void preDestroy()
{
LOGGER.info("AudioManager shutting down");
if(mAudioSegmentProcessorFuture != null)
{
mAudioSegmentProcessorFuture.cancel(true);
mAudioSegmentProcessorFuture = null;
}
mAudioSegmentProcessor.shutdown();
if(mCallRetentionProcessorFuture != null)
{
mCallRetentionProcessorFuture.cancel(true);
mCallRetentionProcessorFuture = null;
}
}
/**
* Adds the call listener to be notified of call events.
* @param listener to add
*/
public void add(ICallEventListener listener)
{
if(!mCallListeners.contains(listener))
{
mCallListeners.add(listener);
}
}
/**
* Broadcasts a call added event to registered listeners.
* @param call that was added
*/
private void broadcastAdded(Call call)
{
for(ICallEventListener listener: mCallListeners)
{
listener.added(call);
}
}
/**
* Broadcasts a call updated event to registered listeners.
* @param call that was updated
*/
private void broadcastUpdated(Call call)
{
for(ICallEventListener listener: mCallListeners)
{
listener.updated(call);
}
}
/**
* Broadcasts a call completed event to registered listeners.
* @param call that was completed
* @param audioSegment associated with the call.
*/
private void broadcastCompleted(Call call, AudioSegment audioSegment)
{
for(ICallEventListener listener: mCallListeners)
{
audioSegment.addLease(listener.getClass().toString());
listener.completed(call, audioSegment);
}
}
/**
* Broadcasts a call deleted event to registered listeners.
* @param call that was deleted
*/
private void broadcastDeleted(Call call)
{
for(ICallEventListener listener: mCallListeners)
{
listener.deleted(call);
}
}
/**
* Removes the listener from receiving call event notifications.
* @param listener to remove
*/
public void remove(ICallEventListener listener)
{
mCallListeners.remove(listener);
}
@Override
public void receive(AudioSegment audioSegment)
{
//Let the duplicate call detector process the segment first to detect duplicates.
audioSegment.addLease(mDuplicateCallDetector.getClass().toString());
mDuplicateCallDetector.receive(audioSegment);
audioSegment.addLease(mAudioSegmentProcessor.getClass().toString());
mAudioSegmentProcessor.add(audioSegment);
//TODO: remove for cleanup
// audioSegment.addLease(mAudioPlaybackManager.getClass().toString());
// mAudioPlaybackManager.receive(audioSegment);
}
/**
* Active audio segment processor. Single thread processor that runs as a scheduled task to process incoming
* audio segments, update existing active audio segments, and process completed audio segments.
*/
private class ActiveAudioSegmentProcessor implements Runnable
{
private LinkedTransferQueue<AudioSegment> mQueuedAudioSegments = new LinkedTransferQueue<>();
private List<AudioSegment> mAudioSegmentsToProcess = new ArrayList<>();
private Map<AudioSegment,ActiveAudioSegment> mActiveAudioSegmentsMap = new HashMap<>();
private AtomicBoolean mProcessing = new AtomicBoolean();
/**
* Adds the new audio segment to be processed
* @param audioSegment to process
*/
public void add(AudioSegment audioSegment)
{
mQueuedAudioSegments.add(audioSegment);
}
/**
* Performs shutdown and cleanup of any active audio segments that remain after the threaded processing
* is terminated.
*/
public void shutdown()
{
for(Map.Entry<AudioSegment,ActiveAudioSegment> entry: mActiveAudioSegmentsMap.entrySet())
{
//Invoke dispose() method to remove the lease on the audio segment
entry.getValue().dispose();
}
mActiveAudioSegmentsMap.clear();
for(AudioSegment audioSegment: mQueuedAudioSegments)
{
audioSegment.removeLease(getClass().toString());
}
mQueuedAudioSegments.clear();
}
@Override
public void run()
{
if(mProcessing.compareAndSet(false, true))
{
//Process existing active audio segments
try
{
Iterator<Map.Entry<AudioSegment,ActiveAudioSegment>> it = mActiveAudioSegmentsMap.entrySet().iterator();
while(it.hasNext())
{
ActiveAudioSegment activeAudioSegment = it.next().getValue();
if(activeAudioSegment.update())
{
broadcastUpdated(activeAudioSegment.getCall());
}
if(activeAudioSegment.isComplete())
{
//Process residual audio and close the wave recording file.
activeAudioSegment.closeAudio();
Call call = activeAudioSegment.getCall();
call.setComplete(true);
try
{
mCallRepository.save(call);
}
catch(Exception e)
{
LOGGER.error("Couldn't update call in database");
}
broadcastCompleted(call, activeAudioSegment.getAudioSegment());
activeAudioSegment.dispose();
it.remove();
}
}
}
catch(Throwable t)
{
LOGGER.error("Error while processing existing active audio segments", t);
}
//Process new audio segments
try
{
mQueuedAudioSegments.drainTo(mAudioSegmentsToProcess);
for(AudioSegment audioSegment: mAudioSegmentsToProcess)
{
if(mActiveAudioSegmentsMap.containsKey(audioSegment))
{
ActiveAudioSegment activeAudioSegment = mActiveAudioSegmentsMap.get(audioSegment);
if(activeAudioSegment.update())
{
broadcastUpdated(activeAudioSegment.getCall());
}
}
else
{
Call call = ActiveAudioSegment.create(audioSegment);
ActiveAudioSegment activeAudioSegment = new ActiveAudioSegment(audioSegment, call, mUserPreferences);
mActiveAudioSegmentsMap.put(audioSegment, activeAudioSegment);
mCallRepository.save(call);
broadcastAdded(call);
}
}
mAudioSegmentsToProcess.clear();
}
catch(Throwable t)
{
LOGGER.error("Error while processing existing active audio segments", t);
}
mProcessing.set(false);
}
}
}
/**
* Call event retention and age-off processor.
*/
public class CallAgeOffProcessor implements Runnable
{
@Override
public void run()
{
RetentionPreference retentionPreference = mUserPreferences.getRetentionPreference();
RetentionPolicy policy = retentionPreference.getRetentionPolicy();
try
{
if(policy.isAgePolicy())
{
AgeUnits ageUnits = retentionPreference.getAgeUnits();
int ageValue = retentionPreference.getAgeValue();
long threshold = System.currentTimeMillis() - ageUnits.getDuration(ageValue);
PageRequest requestPage1 = PageRequest.of(0, 100);
long start = 0;
Page<Call> page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, threshold, requestPage1);
while(page.getTotalElements() > 0)
{
List<Call> calls = page.get().collect(Collectors.toList());
for(Call call: calls)
{
//Keep adjusting the start time to 1 millisecond more than the current call so that
//if there's an error deleting any given call, we don't get stuck processing it every time
start = call.getEventTime() + 1;
//Only evaluate calls that are complete
if(call.isComplete())
{
boolean deleteFailed = false;
File file = new File(call.getFile());
if(file.exists())
{
try
{
File parentDirectory = file.getParentFile();
Files.delete(file.toPath());
//Delete parent directory if it is now empty
if(parentDirectory.exists() && parentDirectory.isDirectory())
{
if(!Files.list(parentDirectory.toPath()).findFirst().isPresent())
{
Files.delete(parentDirectory.toPath());
}
}
}
catch(Exception io)
{
LOGGING_SUPPRESSOR.error("Call Event Delete", 5,
"Error deleting call event recording: " + call.getFile() +
" - " + io.getLocalizedMessage());
deleteFailed = true;
}
}
if(!deleteFailed)
{
mCallRepository.delete(call);
broadcastDeleted(call);
}
}
}
page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, threshold, requestPage1);
}
}
if(policy.isSizePolicy())
{
Path callsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryCalls();
if(Files.exists(callsDirectory))
{
long currentSize = FileUtils.sizeOfDirectory(callsDirectory.toFile());
long objectiveSize = retentionPreference.getObjectiveDirectorySize();
if(currentSize >= objectiveSize)
{
long start = 0;
long now = System.currentTimeMillis();
PageRequest pageable = PageRequest.of(0, 100);
Page<Call> page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, now, pageable);
while(currentSize >= objectiveSize && page.getTotalElements() > 0)
{
List<Call> calls = page.get().collect(Collectors.toList());
for(Call call: calls)
{
//Keep adjusting the start time to 1 millisecond more than the current call so that
//if there's an error deleting any given call, we don't get stuck processing it every time
start = call.getEventTime() + 1;
if(currentSize < objectiveSize)
{
start = now - 1;
break;
}
//Only evaluate calls that are complete
if(call.isComplete())
{
boolean deleteFailed = false;
File file = new File(call.getFile());
if(file.exists())
{
try
{
File parentDirectory = file.getParentFile();
long fileSize = FileUtils.sizeOf(file);
Files.delete(file.toPath());
currentSize -= fileSize;
//Delete parent directory if it is now empty
if(parentDirectory.exists() && parentDirectory.isDirectory())
{
if(!Files.list(parentDirectory.toPath()).findFirst().isPresent())
{
Files.delete(parentDirectory.toPath());
}
}
}
catch(Exception io)
{
LOGGING_SUPPRESSOR.error("Call Event Delete", 5,
"Error deleting call event recording: " + call.getFile() +
" - " + io.getLocalizedMessage());
deleteFailed = true;
}
}
if(!deleteFailed)
{
mCallRepository.delete(call);
broadcastDeleted(call);
}
}
}
page = mCallRepository.findCallsByEventTimeBetweenOrderByEventTime(start, now, pageable);
}
}
}
}
}
catch(Throwable t)
{
LOGGER.error("Unexpected error while deleting call events using retention policy: " + policy, t);
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
@@ -28,12 +29,11 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
import io.github.dsheirer.preference.duplicate.CallManagementPreference;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -42,6 +42,9 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
@@ -49,21 +52,32 @@ import java.util.concurrent.atomic.AtomicBoolean;
* Note: system in this context refers to the system name value that is used in channel configurations. All decoder
* channels must share the same system name for call duplication detection.
*/
@Component("duplicateCallDetector")
public class DuplicateCallDetector implements Listener<AudioSegment>
{
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
private CallManagementPreference mCallManagementPreference;
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
@Resource
private UserPreferences mUserPreferences;
public DuplicateCallDetector(UserPreferences userPreferences)
/**
* Constructs an instance
*/
public DuplicateCallDetector()
{
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
}
@PostConstruct
public void postConstruct()
{
mCallManagementPreference = mUserPreferences.getCallManagementPreference();
}
@Override
public void receive(AudioSegment audioSegment)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
if(mCallManagementPreference.isDuplicateCallDetectionEnabled())
{
Identifier identifier = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
@@ -82,7 +96,9 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
mDetectorMap.put(system, detector);
}
audioSegment.addLease(detector.getClass().toString());
detector.add(audioSegment);
audioSegment.removeLease(getClass().toString());
}
}
}
@@ -137,7 +153,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
*/
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
{
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
if(mCallManagementPreference.isDuplicateCallDetectionByTalkgroupEnabled())
{
//Step 1 check for duplicate TO values
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
@@ -149,7 +165,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
}
}
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
if(mCallManagementPreference.isDuplicateCallDetectionByRadioEnabled())
{
//Step 2 check for duplicate FROM values
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
@@ -242,7 +258,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
if(complete)
{
audioSegment.decrementConsumerCount();
audioSegment.removeLease(getClass().toString());
}
return complete;
@@ -271,7 +287,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
if(isDuplicate(current, toCheck))
{
toCheck.setDuplicate(true);
toCheck.decrementConsumerCount();
toCheck.removeLease(getClass().toString());
duplicates.add(toCheck);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.sample.Listener;
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.sample.Listener;
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,78 +19,82 @@
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.audio.call.Call;
import io.github.dsheirer.audio.call.CompletedCall;
import io.github.dsheirer.audio.call.ICallEventListener;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
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 jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* 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>
@Component("audioStreamingManager")
public class AudioStreamingManager implements ICallEventListener
{
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;
@Resource
private BroadcastModel mBroadcastModel;
@Resource
private UserPreferences mUserPreferences;
private LinkedTransferQueue<CompletedCall> mCompletedCallQueue = new LinkedTransferQueue<>();
private List<CompletedCall> mCompletedCalls = new ArrayList<>();
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
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)
public AudioStreamingManager()
{
mAudioRecordingListener = listener;
mBroadcastFormat = broadcastFormat;
mUserPreferences = userPreferences;
}
/**
* Primary receive method
*/
@Override
public void receive(AudioSegment audioSegment)
{
mNewAudioSegments.add(audioSegment);
}
/**
* Starts the scheduled audio segment processor
*/
@PostConstruct
public void start()
{
if(mAudioSegmentProcessorFuture == null)
{
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 250, TimeUnit.MILLISECONDS);
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new CompletedCallProcessor(),
0, 250, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the scheduled audio segment processor
*/
@PreDestroy
public void stop()
{
if(mAudioSegmentProcessorFuture != null)
@@ -99,73 +103,148 @@ public class AudioStreamingManager implements Listener<AudioSegment>
mAudioSegmentProcessorFuture = null;
}
for(AudioSegment audioSegment: mNewAudioSegments)
for(CompletedCall completedCall: mCompletedCallQueue)
{
audioSegment.decrementConsumerCount();
completedCall.audioSegment().removeLease(getClass().toString());
}
mNewAudioSegments.clear();
mCompletedCallQueue.clear();
for(AudioSegment audioSegment: mAudioSegments)
for(CompletedCall completedCall: mCompletedCalls)
{
audioSegment.decrementConsumerCount();
completedCall.audioSegment().removeLease(getClass().toString());
}
mAudioSegments.clear();
mCompletedCalls.clear();
}
/**
* Implements the ICallEventListener interface to receive and enqueue completed calls to process for streaming.
* @param call that was completed.
* @param audioSegment for the call
*/
@Override
public void completed(Call call, AudioSegment audioSegment)
{
mCompletedCallQueue.add(new CompletedCall(call, audioSegment));
}
@Override
public void added(Call call) {} //Not implemented
@Override
public void updated(Call call) {} //Not implemented
@Override
public void deleted(Call call) {} //Not implemented
/**
* Main processing method to process audio segments
*/
private void processAudioSegments()
private void processCompletedCalls()
{
mNewAudioSegments.drainTo(mAudioSegments);
CompletedCall completedCall = mCompletedCallQueue.poll();
Iterator<AudioSegment> it = mAudioSegments.iterator();
AudioSegment audioSegment;
while(it.hasNext())
while(completedCall != null)
{
audioSegment = it.next();
AudioSegment audioSegment = completedCall.audioSegment();
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
if(!(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled()))
{
it.remove();
audioSegment.decrementConsumerCount();
}
else if(audioSegment.completeProperty().get())
{
it.remove();
if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
if(mBroadcastModel != 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, mUserPreferences);
IdentifierCollection identifierCollectionCopy =
IdentifierCollection identifiers =
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(),
identifierCollectionCopy, audioSegment.getStartTimestamp(), length);
mAudioRecordingListener.receive(audioRecording);
}
catch(IOException ioe)
if(identifiers.getToIdentifier() instanceof PatchGroupIdentifier patchGroupIdentifier)
{
mLog.error("Error recording temporary stream MP3");
if(mUserPreferences.getCallManagementPreference()
.getPatchGroupStreamingOption() == PatchGroupStreamingOption.TALKGROUPS)
{
//Decompose the patch group into the individual (patched) talkgroups and process the audio
//segment for each patched talkgroup.
PatchGroup patchGroup = patchGroupIdentifier.getValue();
List<Identifier> ids = new ArrayList<>();
ids.addAll(patchGroup.getPatchedTalkgroupIdentifiers());
ids.addAll(patchGroup.getPatchedRadioIdentifiers());
//If there are no patched radios/talkgroups, override user preference and stream as a patch group
if(ids.isEmpty() || audioSegment.getAliasList() == null)
{
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
}
else
{
AliasList aliasList = audioSegment.getAliasList();
for(Identifier identifier: ids)
{
List<Alias> aliases = aliasList.getAliases(identifier);
Set<BroadcastChannel> broadcastChannels = new HashSet<>();
for(Alias alias: aliases)
{
broadcastChannels.addAll(alias.getBroadcastChannels());
}
if(!broadcastChannels.isEmpty())
{
MutableIdentifierCollection decomposedIdentifiers =
new MutableIdentifierCollection(identifiers.getIdentifiers());
//Remove patch group TO identifier & replace with the patched talkgroup/radio
decomposedIdentifiers.remove(Role.TO);
decomposedIdentifiers.update(identifier);
processAudioSegment(audioSegment, decomposedIdentifiers, broadcastChannels);
}
}
}
}
else
{
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
}
}
else
{
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
}
}
audioSegment.decrementConsumerCount();
}
audioSegment.removeLease(getClass().toString());
completedCall = mCompletedCallQueue.poll();
}
}
/**
* Processes an audio segment for streaming by creating a temporary MP3 recording and submitting the recording
* to the specific broadcast channel(s).
* @param audioSegment to process for streaming
* @param identifierCollection to use for the streamed audio recording.
* @param broadcastChannels to receive the audio recording
*/
private void processAudioSegment(AudioSegment audioSegment, IdentifierCollection identifierCollection,
Set<BroadcastChannel> broadcastChannels)
{
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, mUserPreferences, identifierCollection);
AudioRecording audioRecording = new AudioRecording(path, broadcastChannels, identifierCollection,
audioSegment.getStartTimestamp(), length);
mBroadcastModel.receive(audioRecording);
}
catch(IOException ioe)
{
mLog.error("Error recording temporary stream MP3");
}
}
@@ -195,20 +274,20 @@ public class AudioStreamingManager implements Listener<AudioSegment>
}
/**
* Scheduled runnable to process audio segments.
* Scheduled runnable to process completed calls.
*/
public class AudioSegmentProcessor implements Runnable
public class CompletedCallProcessor implements Runnable
{
@Override
public void run()
{
try
{
processAudioSegments();
processCompletedCalls();
}
catch(Throwable t)
{
mLog.error("Error processing audio segments for streaming", t);
mLog.error("Error processing completed calls for streaming", t);
}
}
}

View File

@@ -25,6 +25,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
@@ -48,6 +49,7 @@ import java.net.SocketAddress;
@JsonSubTypes({
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
@JsonSubTypes.Type(value = OpenMHzConfiguration.class, name="OpenMHzConfiguration"),
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,13 +22,14 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2AudioStreamingBroadcaster;
@@ -62,26 +63,26 @@ public class BroadcastFactory
switch(configuration.getBroadcastServerType())
{
case BROADCASTIFY_CALL:
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration);
case RDIOSCANNER_CALL:
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration);
case OPENMHZ:
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration);
case BROADCASTIFY:
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
inputAudioFormat, mp3Setting, aliasModel);
inputAudioFormat, mp3Setting);
case ICECAST_TCP:
return new IcecastTCPAudioBroadcaster((IcecastTCPConfiguration) configuration, inputAudioFormat,
mp3Setting, aliasModel);
mp3Setting);
case ICECAST_HTTP:
return new IcecastHTTPAudioBroadcaster((IcecastHTTPConfiguration) configuration, inputAudioFormat,
mp3Setting, aliasModel);
mp3Setting);
case SHOUTCAST_V1:
return new ShoutcastV1AudioBroadcaster((ShoutcastV1Configuration) configuration, inputAudioFormat,
mp3Setting, aliasModel);
mp3Setting);
case SHOUTCAST_V2:
return new ShoutcastV2AudioStreamingBroadcaster((ShoutcastV2Configuration) configuration,
inputAudioFormat, mp3Setting, aliasModel);
inputAudioFormat, mp3Setting);
case UNKNOWN:
default:
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());
@@ -109,6 +110,8 @@ public class BroadcastFactory
return new RdioScannerConfiguration(format);
case BROADCASTIFY:
return new BroadcastifyFeedConfiguration(format);
case OPENMHZ:
return new OpenMHzConfiguration(format);
case ICECAST_HTTP:
return new IcecastHTTPConfiguration(format);
case ICECAST_TCP:

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,12 +20,13 @@ package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.icon.IconModel;
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.util.ThreadPool;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
@@ -45,9 +46,11 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.swing.table.AbstractTableModel;
@Component("broadcastModel")
public class BroadcastModel extends AbstractTableModel implements Listener<AudioRecording>
{
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
@@ -68,28 +71,30 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
public static final String[] COLUMN_NAMES = new String[]
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
@Resource
private AliasModel mAliasModel;
@Resource
private UserPreferences mUserPreferences;
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
private List<AudioRecording> mRecordingQueue = new CopyOnWriteArrayList<>();
private Map<Integer,AbstractAudioBroadcaster> mBroadcasterMap = new HashMap<>();
private IconModel mIconModel;
private AliasModel mAliasModel;
private Broadcaster<BroadcastEvent> mBroadcastEventBroadcaster = new Broadcaster<>();
private BroadcastEventListener mBroadcastEventListener = new BroadcastEventListener();
private UserPreferences mUserPreferences;
/**
* Model for managing Broadcast configurations and any associated broadcaster instances.
*/
public BroadcastModel(AliasModel aliasModel, IconModel iconModel, UserPreferences userPreferences)
public BroadcastModel()
{
mAliasModel = aliasModel;
mIconModel = iconModel;
mUserPreferences = userPreferences;
}
@PostConstruct
public void postConstruct()
{
//Monitor to remove temporary recording files that have been streamed by all audio broadcasters
ThreadPool.SCHEDULED.scheduleAtFixedRate(new RecordingDeletionMonitor(), 15l, 15l, TimeUnit.SECONDS);
removeOrphanedTemporaryRecordings();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -31,8 +31,9 @@ public enum BroadcastServerType
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
OPENMHZ("OpenMHz", "images/openmhz.png"),
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
UNKNOWN("Unknown", null);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,10 +20,12 @@ package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.icon.Icon;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import net.miginfocom.swing.MigLayout;
import javax.swing.ImageIcon;
@@ -42,23 +44,15 @@ public class BroadcastStatusPanel extends JPanel
private JTable mTable;
private JTableColumnWidthMonitor mColumnWidthMonitor;
private JScrollPane mScrollPane;
@Resource
private BroadcastModel mBroadcastModel;
private UserPreferences mUserPreferences;
private String mPreferenceKey;
private String mPreferenceKey = "application.broadcast.status.panel";
/**
* Constructs an instance
* @param broadcastModel to access the streams
* @param userPreferences for configuring the panel
* @param preferenceKey to store column preferences for this panel.
*/
public BroadcastStatusPanel(BroadcastModel broadcastModel, UserPreferences userPreferences, String preferenceKey)
public BroadcastStatusPanel()
{
mBroadcastModel = broadcastModel;
mUserPreferences = userPreferences;
mPreferenceKey = preferenceKey;
init();
}
public JTable getTable()
@@ -66,21 +60,19 @@ public class BroadcastStatusPanel extends JPanel
return mTable;
}
private void init()
@PostConstruct
private void postConstruct()
{
setPreferredSize(new Dimension(880, 70));
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
mTable = new JTable(mBroadcastModel);
mTable.setEnabled(false);
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class);
renderer.setHorizontalAlignment(SwingConstants.CENTER);
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS).setCellRenderer(new StatusCellRenderer());
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCAST_SERVER_TYPE).setCellRenderer(new ServerTypeRenderer());
mColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, mPreferenceKey);
mColumnWidthMonitor = new JTableColumnWidthMonitor(mTable, mPreferenceKey);
mScrollPane = new JScrollPane(mTable);
add(mScrollPane);
}

View File

@@ -0,0 +1,48 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast;
/**
* Options for streaming management of patch groups. These options are used when streaming an audio call that is
* tagged to a patch group. The audio will either be streamed once and identified as the patch group, or it will
* be streamed multiple times, once for each individual patched talkgroup.
*/
public enum PatchGroupStreamingOption
{
PATCH_GROUP("Patch Group"),
TALKGROUPS("Individual Talkgroups");
private String mLabel;
/**
* Constructs an instance
* @param label to display
*/
PatchGroupStreamingOption(String label)
{
mLabel = label;
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -20,13 +20,10 @@
package io.github.dsheirer.audio.broadcast.broadcastify;
import com.google.common.net.HttpHeaders;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
@@ -80,10 +77,8 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
/**
* Constructs an instance of the broadcaster
* @param config to use
* @param aliasModel for access to aliases
*/
public BroadcastifyCallBroadcaster(BroadcastifyCallConfiguration config, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
public BroadcastifyCallBroadcaster(BroadcastifyCallConfiguration config)
{
super(config);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
*/
package io.github.dsheirer.audio.broadcast.icecast;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
@@ -28,13 +27,11 @@ import io.github.dsheirer.audio.convert.MP3Setting;
public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
{
private IBroadcastMetadataUpdater mMetadataUpdater;
protected AliasModel mAliasModel;
public IcecastAudioBroadcaster(BroadcastConfiguration broadcastConfiguration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
MP3Setting mp3Setting)
{
super(broadcastConfiguration, inputAudioFormat, mp3Setting);
mAliasModel = aliasModel;
}
/**
@@ -50,7 +47,7 @@ public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration(), mAliasModel);
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration());
}
return mMetadataUpdater;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,23 +18,12 @@
*/
package io.github.dsheirer.audio.broadcast.icecast;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastMetadata;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
@@ -42,7 +31,8 @@ import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
@@ -50,6 +40,7 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
private final static String UTF8 = "UTF-8";
private HttpClient mHttpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
private IcecastConfiguration mIcecastConfiguration;
@Resource
private AliasModel mAliasModel;
private boolean mConnectionLoggingSuppressed = false;
@@ -59,10 +50,9 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
* sequence, those updates will be queued and processed in the order received.
*/
public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration, AliasModel aliasModel)
public IcecastBroadcastMetadataUpdater(IcecastConfiguration icecastConfiguration)
{
mIcecastConfiguration = icecastConfiguration;
mAliasModel = aliasModel;
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,6 +26,7 @@ import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.Arrays;
@@ -54,7 +55,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private static final String HTTP_1_0_OK_HEX_DUMP = "48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B";
@Resource
private AliasModel mAliasModel;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private Map<String,String> mHTTPHeaders;
@@ -74,9 +76,9 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
* @param configuration for the Icecast stream
*/
public IcecastHTTPAudioBroadcaster(IcecastHTTPConfiguration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
MP3Setting mp3Setting)
{
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
super(configuration, inputAudioFormat, mp3Setting);
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@ import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
@@ -51,7 +52,8 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds
private static final int WRITE_TIMEOUT_SECONDS = 5;
@Resource
private AliasModel mAliasModel;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
@@ -70,9 +72,9 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
* @param configuration for the Icecast stream
*/
public IcecastTCPAudioBroadcaster(IcecastTCPConfiguration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
MP3Setting mp3Setting)
{
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
super(configuration, inputAudioFormat, mp3Setting);
}
/**

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* 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.openmhz;
/**
* HTTP headers used for posting to Rdio Scanner API
*/
public enum FormField
{
CALL("call"),
FREQ("freq"),
START_TIME("start_time"),
STOP_TIME("stop_time"),
CALL_LENGTH("call_length"),
TALKGROUP_NUM("talkgroup_num"),
EMERGENCY("emergency"),
API_KEY("api_key"),
SOURCE_LIST("source_list"),
FREQ_LIST("freq_list"),
PATCHES("patches");
private String mHeader;
FormField(String header)
{
mHeader = header;
}
public String getHeader()
{
return mHeader;
}
}

View File

@@ -0,0 +1,636 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.openmhz;
import com.google.common.net.HttpHeaders;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
*
*/
public class OpenMHzBroadcaster extends AbstractAudioBroadcaster<OpenMHzConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzBroadcaster.class);
private static final String ENCODING_TYPE_MP3 = "mp3";
private static final String MULTIPART_TYPE = "multipart";
private static final String DEFAULT_SUBTYPE = "form-data";
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
private HttpClient mHttpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
@Resource
private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
* @param config to use
*/
public OpenMHzBroadcaster(OpenMHzConfiguration config)
{
super(config);
}
/**
* Starts the audio recording processor thread
*/
@Override
public void start()
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
if(response == "OK")
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if (response == "No Response")
{
setBroadcastState(BroadcastState.NO_SERVER);
mLog.error("Error connecting to OpenMHz server [Server not found or not reachable]");
}
else
{
mLog.error("Error connecting to OpenMHz server on startup [" + response + "]");
setBroadcastState(BroadcastState.ERROR);
}
if(mAudioRecordingProcessorFuture == null)
{
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
0, 500, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the audio recording processor thread
*/
@Override
public void stop()
{
if(mAudioRecordingProcessorFuture != null)
{
mAudioRecordingProcessorFuture.cancel(true);
mAudioRecordingProcessorFuture = null;
dispose();
setBroadcastState(BroadcastState.DISCONNECTED);
}
}
/**
* Prepares for disposal
*/
@Override
public void dispose()
{
AudioRecording audioRecording = mAudioRecordingQueue.poll();
while(audioRecording != null)
{
audioRecording.removePendingReplay();
audioRecording = mAudioRecordingQueue.poll();
}
}
/**
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
* the audio processor thread will persistently invoke this method to attempt a reconnect.
*/
private boolean connected()
{
if(getBroadcastState() != BroadcastState.CONNECTED &&
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
if(response == "OK")
{
setBroadcastState(BroadcastState.CONNECTED);
}
else if (response == "No Response")
{
setBroadcastState(BroadcastState.NO_SERVER);
mLog.error("Error reconnecting to OpenMHz server [Server not found or not reachable]");
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.error("Error reconnecting to OpenMHz server [" + response + "]");
}
}
return getBroadcastState() == BroadcastState.CONNECTED;
}
@Override
public int getAudioQueueSize()
{
return mAudioRecordingQueue.size();
}
@Override
public void receive(AudioRecording audioRecording)
{
mAudioRecordingQueue.offer(audioRecording);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
}
/**
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
* deleted to ensure that the in-memory queue size doesn't blow up.
* @param audioRecording to test
* @return true if the recording is valid
*/
private boolean isValid(AudioRecording audioRecording)
{
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
getBroadcastConfiguration().getMaximumRecordingAge();
}
/**
* Processes any enqueued audio recordings. This method employs asynchronous
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
*/
private void processRecordingQueue()
{
while(connected() && !mAudioRecordingQueue.isEmpty())
{
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
{
int durationSeconds = (int)(audioRecording.getRecordingLength() / 1E3f);
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
String talkgroup = getTo(audioRecording);
String radioId = getFrom(audioRecording);
Long frequency = getFrequency(audioRecording);
String patches = getPatches(audioRecording);
String talkgroupLabel = getTalkgroupLabel(audioRecording);
String talkgroupGroup = getTalkgroupGroup(audioRecording);
String systemLabel = getSystemLabel(audioRecording);
try
{
byte[] audioBytes = null;
try
{
audioBytes = Files.readAllBytes(audioRecording.getPath());
}
catch(IOException e)
{
mLog.error("OpenMHz - audio recording file not found - ignoring upload");
}
if(audioBytes != null)
{
String uri = getBroadcastConfiguration().getHost() + "/" + getBroadcastConfiguration().getSystemName() + "/upload";
OpenMHzBuilder bodyBuilder = new OpenMHzBuilder();
bodyBuilder
.addFile(audioBytes)
.addPart(FormField.FREQ, frequency)
.addPart(FormField.START_TIME, timestampSeconds)
.addPart(FormField.STOP_TIME, timestampSeconds)
.addPart(FormField.CALL_LENGTH, durationSeconds)
.addPart(FormField.TALKGROUP_NUM, talkgroup)
.addPart(FormField.EMERGENCY, 0)
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
// For future use if OpenMHz supports patch information
//.addPart(FormField.PATCHES, patches)
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
HttpRequest fileRequest = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.POST(bodyBuilder.build())
.build();
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
.whenComplete((fileResponse, throwable1) -> {
if(throwable1 != null || fileResponse.statusCode() != 200)
{
if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
{
//We get socket reset exceptions occasionally when the remote server doesn't
//fully read our request and immediately responds.
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("OpenMHz API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("OpenMHz API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
incrementErrorAudioCount();
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
}
else
{
if(fileResponse.statusCode() == 200)
{
incrementStreamedAudioCount();
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("OpenMHz API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
}
});
}
else
{
//Register an error for the file not found exception
mLog.error("OpenMHz API - upload file not found [" +
audioRecording.getPath().toString() + "]");
incrementErrorAudioCount();
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
}
catch(Exception e)
{
mLog.error("Unknown Error", e);
setBroadcastState(BroadcastState.ERROR);
incrementErrorAudioCount();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
}
}
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
AudioRecording audioRecording = mAudioRecordingQueue.peek();
while(audioRecording != null)
{
if(isValid(audioRecording))
{
return;
}
else
{
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
mAudioRecordingQueue.poll();
audioRecording.removePendingReplay();
incrementAgedOffAudioCount();
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
audioRecording = mAudioRecordingQueue.peek();
}
}
}
/**
* Creates a frequency value from the audio recording identifier collection.
*/
private static Long getFrequency(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
Form.CHANNEL_FREQUENCY, Role.ANY);
if(identifier instanceof ConfigurationLongIdentifier)
{
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
if(value != null)
{
return value;
}
}
return Long.valueOf(0);
}
/**
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
*/
private static String getFrom(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof RadioIdentifier)
{
return ((RadioIdentifier)identifier).getValue().toString();
}
}
return "0";
}
/**
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*
*/
private static String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
}
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
}
return "0";
}
/**
* Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
* If this is a PatchGroup we return only the first label as the primary talkgroup label.
*
*/
private String getTalkgroupLabel(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
StringBuilder sb = new StringBuilder();
if(identifier != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
if(!aliases.isEmpty())
{
sb.append(aliases.get(0));
}
}
return sb.toString();
}
/**
* Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
* If this is a PatchGroup we return only the first group as the primary talkgroup group.
*
*/
private String getTalkgroupGroup(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
StringBuilder sb = new StringBuilder();
if(identifier != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
if(!aliases.isEmpty())
{
sb.append(aliases.get(0).getGroup());
}
}
return sb.toString();
}
/**
* Creates a formatted string with the System Label from the Audio Recording Alias
* If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
*
*/
private String getSystemLabel(AudioRecording audioRecording)
{
List<Identifier> systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
StringBuilder sb = new StringBuilder();
if(!systems.isEmpty())
{
sb.append(systems.get(0));
}
return sb.toString();
}
/**
* Creates a formatted string with the patched talkgroups
*/
public static String getPatches(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
PatchGroup patchGroup = patchGroupIdentifier.getValue();
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{
sb.append(",").append(patched.getValue());
}
sb.append("]");
return sb.toString();
}
return "[]";
}
/**
* Tests both the connection and configuration against the OpenMHz Call API service
* @param configuration containing API key and system id
* @return error string or null if test is successful
*/
public static String testConnection(OpenMHzConfiguration configuration)
{
String uri = configuration.getHost() + "/" + configuration.getSystemName() + "/authorize";
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(uri))
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_FORM_URLENCODED)
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.POST(HttpRequest.BodyPublishers.ofString("api_key="+configuration.getApiKey()))
.build();
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
try
{
HttpResponse<String> response = httpClient.send(request, responseHandler);
if (response.statusCode() == 200)
{
return "OK";
}
else if(response.statusCode() == 403)
{
return "Invalid API Key";
}
else if(response.statusCode() == 500)
{
return "Invalid System Name";
}
return "No Response";
}
catch(Exception e)
{
Throwable throwableCause = e.getCause();
if(throwableCause instanceof ConnectException)
{
return "No Response";
}
mLog.error("Exception connecting to OpenMHz server [" + e.toString() + "]");
return "Uknown Exception";
}
}
public class AudioRecordingProcessor implements Runnable
{
@Override
public void run()
{
processRecordingQueue();
}
}
public static void main(String[] args)
{
mLog.debug("Starting ...");
OpenMHzConfiguration config = new OpenMHzConfiguration();
config.setHost("https://api.OpenMHz.com/call-upload-dev");
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
config.setSystemName("systemx");
String response = testConnection(config);
mLog.error("Response: " + response);
if(response == "OK")
{
mLog.debug("Test Successful!");
}
else
{
if(response.contains("Invalid API Key"))
{
mLog.error("Invalid API Key");
}
else if(response.contains("Invalid System Name"))
{
mLog.error("Invalid System Name");
}
else
{
mLog.debug("Response: " + response);
}
}
mLog.debug("Finished!");
}
}

View File

@@ -0,0 +1,216 @@
/*
* *****************************************************************************
* 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.openmhz;
import java.net.http.HttpRequest;
import java.util.ArrayList;
import java.util.List;
import java.lang.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* Builder for an HTTP body publisher to produce a OpenMHz call event
*/
public class OpenMHzBuilder
{
private static final String DASH_DASH = "--";
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
private List<Part> mParts = new ArrayList<>();
private byte[] audioBytes = null;
/**
* Constructs an instance
*/
public OpenMHzBuilder()
{
}
/**
* Access the static multi-part boundary string
*/
public String getBoundary()
{
return BOUNDARY;
}
/**
* Adds a Audio bytes part to the call
*/
public OpenMHzBuilder addFile(byte[] value)
{
audioBytes = value;
return this;
}
/**
* Adds a string part to the call
*/
public OpenMHzBuilder addPart(FormField key, String value)
{
if(key != null && value != null)
{
mParts.add(new Part(key.getHeader(), value));
}
return this;
}
/**
* Adds a number part to the call
*/
public OpenMHzBuilder addPart(FormField key, Number value)
{
if(key != null && value != null)
{
mParts.add(new Part(key.getHeader(), value.toString()));
}
return this;
}
/**
* Creates a form-data item
*/
private static String formatPart(Part part, String boundary)
{
StringBuilder sb = new StringBuilder();
sb.append(DASH_DASH).append(boundary).append("\r\n");
sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
sb.append(part.getValue()).append("\r\n");
return sb.toString();
}
/**
* Creates the audio file item
*/
private String formatFilePart(String boundary)
{
if(audioBytes == null)
{
return "";
}
StringBuilder sb= new StringBuilder();
sb.append(DASH_DASH).append(boundary).append("\r\n");
sb.append("Content-Disposition: form-data; name=\"").append("call").append("\"; filename=\"audio.mp3\"\r\n\r\n");
return sb.toString();
}
/**
* Creates the boundary closing item
*/
private static String getClosingBoundary(String boundary)
{
StringBuilder sb = new StringBuilder();
sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
return sb.toString();
}
/**
* Creates a BodyPublisher for accessing the call form data
*/
public HttpRequest.BodyPublisher build()
{
StringBuilder sb = new StringBuilder();
for(Part part: mParts)
{
sb.append(formatPart(part, BOUNDARY));
}
sb.append(formatFilePart(BOUNDARY));
/**
* We need to create a ByteArray consisting of the Sting "parts" and the audio file bytes
*/
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try
{
outputStream.write(sb.toString().getBytes());
if(audioBytes != null)
{
outputStream.write(audioBytes);
}
sb = new StringBuilder();
sb.append("\r\n");
sb.append(getClosingBoundary(BOUNDARY));
outputStream.write(sb.toString().getBytes());
}
catch(IOException e)
{
//mLog.error("Rdio Scanner API - unable to create POST reqeust.");
}
return HttpRequest.BodyPublishers.ofByteArray(outputStream.toByteArray());
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
for(Part part: mParts)
{
sb.append(formatPart(part, BOUNDARY));
}
sb.append(getClosingBoundary(BOUNDARY));
return sb.toString();
}
/**
* Key:Value pair holder
*/
public class Part
{
private String mKey;
private String mValue;
/**
* Constructs a new part
* @param key value
* @param value item
*/
public Part(String key, String value)
{
mKey = key;
mValue = value;
}
public String getKey()
{
return mKey;
}
public String getValue()
{
return mValue;
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* *****************************************************************************
* 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.openmhz;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* Streaming configuration for Rdio Scanner API.
*
* Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
* with the structure of the audio streaming subsystem in sdrtrunk
*/
public class OpenMHzConfiguration extends BroadcastConfiguration
{
public static final String PRODUCTION_ENDPOINT = "https://api.openmhz.com";
private StringProperty mSystemName = new SimpleStringProperty();
private StringProperty mApiKey = new SimpleStringProperty();
/**
* Constructor for faster jackson
*/
public OpenMHzConfiguration()
{
this(BroadcastFormat.MP3);
}
/**
* Public constructor.
* @param format to use for audio recording (MP3)
*/
public OpenMHzConfiguration(BroadcastFormat format)
{
super(format);
if(getHost() == null)
{
setHost(PRODUCTION_ENDPOINT);
}
//The parent class binds this property, so we unbind it and rebind it here
mValid.unbind();
mValid.bind(Bindings.and(Bindings.and(Bindings.isNotNull(mSystemName), Bindings.isNotNull(mApiKey)),
Bindings.isNotNull(mHost)));
}
/**
* System ID as a property
*/
public StringProperty systemIDProperty()
{
return mSystemName;
}
/**
* API key as a property
*/
public StringProperty apiKeyProperty()
{
return mApiKey;
}
/**
* API Key
*/
@JacksonXmlProperty(isAttribute = true, localName = "api_key")
public String getApiKey()
{
return mApiKey.get();
}
/**
* Sets the api key
* @param apiKey
*/
public void setApiKey(String apiKey)
{
mApiKey.setValue(apiKey);
}
/**
* System name as provided by OpenMHz.com
*/
@JacksonXmlProperty(isAttribute = true, localName = "system_name")
public String getSystemName()
{
return mSystemName.get();
}
/**
* Sets the system name provided by OpenMHz.com
*/
public void setSystemName(String systemName)
{
mSystemName.set(systemName);
}
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.OPENMHZ;
}
@Override
public BroadcastConfiguration copyOf()
{
OpenMHzConfiguration copy = new OpenMHzConfiguration();
copy.setSystemName(getSystemName());
return copy;
}
}

View File

@@ -0,0 +1,225 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.openmhz;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.gui.playlist.streaming.AbstractBroadcastEditor;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* OpenMHz calls API configuration editor
*/
public class OpenMHzEditor extends AbstractBroadcastEditor<OpenMHzConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzEditor.class);
private TextField mSystemNameTextField;
private IntegerTextField mMaxAgeTextField;
private TextField mApiKeyTextField;
private TextField mHostTextField;
private GridPane mEditorPane;
/**
* Constructs an instance
*/
public OpenMHzEditor()
{
}
@Override
public void setItem(OpenMHzConfiguration item)
{
super.setItem(item);
getSystemNameTextField().setDisable(item == null);
getApiKeyTextField().setDisable(item == null);
getHostTextField().setDisable(item == null);
getMaxAgeTextField().setDisable(item == null);
if(item != null)
{
getSystemNameTextField().setText(item.getSystemName());
getApiKeyTextField().setText(item.getApiKey());
getHostTextField().setText(item.getHost());
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
}
else
{
getSystemNameTextField().setText(null);
getApiKeyTextField().setText(null);
getHostTextField().setText(null);
getMaxAgeTextField().set(0);
}
modifiedProperty().set(false);
}
@Override
public void dispose()
{
}
@Override
public void save()
{
if(getItem() != null)
{
getItem().setSystemName(getSystemNameTextField().getText());
getItem().setHost(getHostTextField().getText());
getItem().setApiKey(getApiKeyTextField().getText());
getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
}
super.save();
}
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.OPENMHZ;
}
protected GridPane getEditorPane()
{
if(mEditorPane == null)
{
mEditorPane = new GridPane();
mEditorPane.setPadding(new Insets(10, 5, 10,10));
mEditorPane.setVgap(10);
mEditorPane.setHgap(5);
int row = 0;
Label formatLabel = new Label("Format");
GridPane.setHalignment(formatLabel, HPos.RIGHT);
GridPane.setConstraints(formatLabel, 0, row);
mEditorPane.getChildren().add(formatLabel);
GridPane.setConstraints(getFormatField(), 1, row);
mEditorPane.getChildren().add(getFormatField());
Label enabledLabel = new Label("Enabled");
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
GridPane.setConstraints(enabledLabel, 2, row);
mEditorPane.getChildren().add(enabledLabel);
GridPane.setConstraints(getEnabledSwitch(), 3, row);
mEditorPane.getChildren().add(getEnabledSwitch());
Label systemLabel = new Label("Name");
GridPane.setHalignment(systemLabel, HPos.RIGHT);
GridPane.setConstraints(systemLabel, 0, ++row);
mEditorPane.getChildren().add(systemLabel);
GridPane.setConstraints(getNameTextField(), 1, row);
mEditorPane.getChildren().add(getNameTextField());
Label apiKeyLabel = new Label("API Key");
GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
GridPane.setConstraints(apiKeyLabel, 0, ++row);
mEditorPane.getChildren().add(apiKeyLabel);
GridPane.setConstraints(getApiKeyTextField(), 1, row);
mEditorPane.getChildren().add(getApiKeyTextField());
Label systemNameLabel = new Label("System Short Name");
GridPane.setHalignment(systemNameLabel, HPos.RIGHT);
GridPane.setConstraints(systemNameLabel, 0, ++row);
mEditorPane.getChildren().add(systemNameLabel);
GridPane.setConstraints(getSystemNameTextField(), 1, row);
mEditorPane.getChildren().add(getSystemNameTextField());
Label hostLabel = new Label("OpenMHz Upload Server (API)");
GridPane.setHalignment(hostLabel, HPos.RIGHT);
GridPane.setConstraints(hostLabel, 0, ++row);
mEditorPane.getChildren().add(hostLabel);
GridPane.setConstraints(getHostTextField(), 1, row);
mEditorPane.getChildren().add(getHostTextField());
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
GridPane.setConstraints(maxAgeLabel, 0, ++row);
mEditorPane.getChildren().add(maxAgeLabel);
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
mEditorPane.getChildren().add(getMaxAgeTextField());
}
return mEditorPane;
}
private IntegerTextField getMaxAgeTextField()
{
if(mMaxAgeTextField == null)
{
mMaxAgeTextField = new IntegerTextField();
mMaxAgeTextField.setDisable(true);
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
}
return mMaxAgeTextField;
}
private TextField getHostTextField()
{
if(mHostTextField == null)
{
mHostTextField = new TextField();
mHostTextField.setDisable(true);
mHostTextField.textProperty().addListener(mEditorModificationListener);
}
return mHostTextField;
}
private TextField getApiKeyTextField()
{
if(mApiKeyTextField == null)
{
mApiKeyTextField = new TextField();
mApiKeyTextField.setDisable(true);
mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
}
return mApiKeyTextField;
}
private TextField getSystemNameTextField()
{
if(mSystemNameTextField == null)
{
mSystemNameTextField = new TextField();
mSystemNameTextField.setDisable(true);
mSystemNameTextField.textProperty().addListener(mEditorModificationListener);
}
return mSystemNameTextField;
}
}

View File

@@ -0,0 +1,126 @@
/*
*
* * ******************************************************************************
* * 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.openmhz;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OpenMHzFeedConfiguration extends IcecastTCPConfiguration
{
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzFeedConfiguration.class);
private int mFeedID;
public OpenMHzFeedConfiguration()
{
//No-arg constructor for JAXB
this(BroadcastFormat.MP3);
}
/**
* OpenMHz configuration for Icecast 2.3.2 compatible servers
*
* @param format of output audio (MP3)
*/
public OpenMHzFeedConfiguration(BroadcastFormat format)
{
super(format);
setBitRate(16);
setChannels(1);
setSampleRate(8000);
setInline(true);
}
public static OpenMHzFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
{
OpenMHzFeedConfiguration config = new OpenMHzFeedConfiguration(BroadcastFormat.MP3);
config.setName(userFeedBroadcast.getDescription());
config.setHost(userFeedBroadcast.getHostname());
config.setMountPoint(userFeedBroadcast.getMount());
config.setFeedID(userFeedBroadcast.getFeedId());
config.setPassword(userFeedBroadcast.getPassword());
try
{
config.setPort(Integer.parseInt(userFeedBroadcast.getPort()));
}
catch(Exception e)
{
mLog.error("Error creating Rdio Scanner configuration from radio reference user feed instance");
}
return config;
}
@Override
public BroadcastConfiguration copyOf()
{
OpenMHzFeedConfiguration copy = new OpenMHzFeedConfiguration(getBroadcastFormat());
//Broadcast Configuration Parameters
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setInline(getInline());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters
copy.setUserName(getUserName());
copy.setMountPoint(getMountPoint());
copy.setDescription(getDescription());
copy.setGenre(getGenre());
copy.setPublic(isPublic());
copy.setURL(getURL());
//OpenMHz Configuration Parameters
copy.setFeedID(getFeedID());
return copy;
}
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.OPENMHZ;
}
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
public int getFeedID()
{
return mFeedID;
}
public void setFeedID(int feedID)
{
mFeedID = feedID;
}
}

View File

@@ -27,8 +27,6 @@ import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
@@ -40,6 +38,7 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
@@ -78,18 +77,16 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
@Resource
private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
* @param config to use
* @param aliasModel for access to aliases
*/
public RdioScannerBroadcaster(RdioScannerConfiguration config, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
public RdioScannerBroadcaster(RdioScannerConfiguration config)
{
super(config);
mAliasModel = aliasModel;
}
/**
@@ -232,6 +229,9 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
String talkgroupLabel = getTalkgroupLabel(audioRecording);
String talkgroupGroup = getTalkgroupGroup(audioRecording);
String systemLabel = getSystemLabel(audioRecording);
String path = audioRecording.getPath().toString();
// Remove TEMPORARY_STREAM_FILE_SUFFIX
String audioName = path.substring(path.substring(0, path.lastIndexOf("_")).lastIndexOf("_") + 1);
try
{
@@ -252,6 +252,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
.addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
.addAudioName(audioName)
.addFile(audioBytes)
.addPart(FormField.DATE_TIME, timestampSeconds)
.addPart(FormField.TALKGROUP_ID, talkgroup)
@@ -266,7 +267,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
.uri(URI.create(getBroadcastConfiguration().getHost()))
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
.POST(bodyBuilder.build())
.build();
@@ -437,7 +437,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
*/
private String getTalkgroupLabel(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
@@ -462,7 +461,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
*/
private String getTalkgroupGroup(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();

View File

@@ -34,7 +34,8 @@ public class RdioScannerBuilder
private static final String DASH_DASH = "--";
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
private List<Part> mParts = new ArrayList<>();
private byte[] audioBytes = null;
private byte[] audioBytes = null;
private String audioName = null;
/**
* Constructs an instance
@@ -51,6 +52,15 @@ public class RdioScannerBuilder
return BOUNDARY;
}
/**
* Adds a Audio file name part to the call
*/
public RdioScannerBuilder addAudioName(String value)
{
audioName = value;
return this;
}
/**
* Adds a Audio bytes part to the call
*/
@@ -110,7 +120,8 @@ public class RdioScannerBuilder
}
StringBuilder sb= new StringBuilder();
sb.append(DASH_DASH).append(boundary).append("\r\n");
sb.append("Content-Disposition: form-data; name=\"").append("audio").append("\"\r\n\r\n");
sb.append("Content-Disposition: form-data; filename=\"").append(audioName);
sb.append("\"; name=\"").append("audio").append("\"\r\n\r\n");
return sb.toString();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,6 @@
*/
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
@@ -26,6 +25,10 @@ import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.util.ThreadPool;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
@@ -37,11 +40,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1AudioBroadcaster.class);
@@ -50,7 +48,6 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private IBroadcastMetadataUpdater mMetadataUpdater;
private AliasModel mAliasModel;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
@@ -65,10 +62,9 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
* @param configuration for the Shoutcast stream
*/
public ShoutcastV1AudioBroadcaster(ShoutcastV1Configuration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
MP3Setting mp3Setting)
{
super(configuration, inputAudioFormat, mp3Setting);
mAliasModel = aliasModel;
}
/**
@@ -84,7 +80,7 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getConfiguration(), mAliasModel);
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getConfiguration());
}
return mMetadataUpdater;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,17 @@ import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
@@ -41,23 +52,13 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
private final static String UTF8 = "UTF-8";
private ShoutcastV1Configuration mShoutcastV1Configuration;
@Resource
private AliasModel mAliasModel;
private NioSocketConnector mSocketConnector;
private AtomicBoolean mUpdating = new AtomicBoolean();
@@ -70,10 +71,9 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
* broadcaster. When multiple metadata updates are received prior to completion of the current ongoing update
* sequence, those updates will be queued and processed in the order received.
*/
public ShoutcastV1BroadcastMetadataUpdater(ShoutcastV1Configuration shoutcastV1Configuration, AliasModel aliasModel)
public ShoutcastV1BroadcastMetadataUpdater(ShoutcastV1Configuration shoutcastV1Configuration)
{
mShoutcastV1Configuration = shoutcastV1Configuration;
mAliasModel = aliasModel;
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -49,6 +49,15 @@ import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.math3.util.FastMath;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
@@ -59,25 +68,17 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class);
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private int mMaxPayloadSize = 16377;
@Resource
private AliasModel mAliasModel;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private AliasModel mAliasModel;
private long mLastConnectionAttempt = 0;
private int mMaxPayloadSize = 16377;
private AtomicBoolean mConnecting = new AtomicBoolean();
private LinkedTransferQueue<UltravoxMessage> mMetadataMessageQueue = new LinkedTransferQueue<>();
@@ -94,10 +95,9 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas
* @param configuration for the Shoutcast V2 stream
*/
public ShoutcastV2AudioStreamingBroadcaster(ShoutcastV2Configuration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
MP3Setting mp3Setting)
{
super(configuration, inputAudioFormat, mp3Setting);
mAliasModel = aliasModel;
}
public ShoutcastV2Configuration getConfiguration()

View File

@@ -0,0 +1,461 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ChannelNameConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.SiteConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.wave.AudioMetadata;
import io.github.dsheirer.record.wave.AudioMetadataUtils;
import io.github.dsheirer.record.wave.WaveWriter;
import io.github.dsheirer.sample.ConversionUtils;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages an active audio segment through to completion and provides options for refreshing the associated call
* object.
*/
public class ActiveAudioSegment
{
private static final Logger LOGGER = LoggerFactory.getLogger(ActiveAudioSegment.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOGGER);
private static final SimpleDateFormat DIRECTORY_SDF = new SimpleDateFormat("yyyyMMdd");
private static final SimpleDateFormat FILE_NAME_SDF = new SimpleDateFormat("yyyyMMdd_HHmmss");
private static int sFileNameCounter = 1;
private AudioSegment mAudioSegment;
private Call mCall;
private UserPreferences mUserPreferences;
private WaveWriter mWaveWriter;
private Path mPath;
private int mAudioBufferPointer = 0;
/**
* Constructs an instance
* @param audioSegment
* @param call
* @param userPreferences for lookup of recording directory
*/
public ActiveAudioSegment(AudioSegment audioSegment, Call call, UserPreferences userPreferences)
{
mAudioSegment = audioSegment;
mAudioSegment.addLease(getClass().toString());
mCall = call;
mUserPreferences = userPreferences;
mPath = getAudioRecordingPath();
mCall.setFile(mPath.toString());
}
/**
* Performs disposal workflow.
*/
public void dispose()
{
if(mWaveWriter != null)
{
closeAudio();
}
mCall.setComplete(true);
mAudioSegment.removeLease(getClass().toString());
mAudioSegment = null;
mCall = null;
}
/**
* Audio Segment that is active.
* @return audio segment
*/
public AudioSegment getAudioSegment()
{
return mAudioSegment;
}
/**
* Call entity associated with this audio segment
* @return call entity
*/
public Call getCall()
{
return mCall;
}
/**
* Indicates if this audio segment is active and continues to receive audio buffer and identifier updates.
* @return true if active or false if the audio segment is complete.
*/
public boolean isComplete()
{
return getAudioSegment().isComplete();
}
/**
* Attempts to update/refresh the call entity with details from the audio segment and returns true if any details in
* the call entity were updated or false otherwise.
* @return status of the update, true if updated or false if no update.
*/
public boolean update()
{
boolean updated = false;
updated |= updateTo(mCall, mAudioSegment.getIdentifierCollection().getToIdentifier(), getAudioSegment().getAliasList());
updated |= updateFrom(mCall, mAudioSegment.getIdentifierCollection().getFromIdentifier(), getAudioSegment().getAliasList());
updated |= updateDuration(mCall, mAudioSegment.getDuration());
updated |= updateFlags(mCall, mAudioSegment);
updated |= processAudio();
return updated;
}
/**
* Generates a unique audio recording file path.
* @return audio recording path.
*/
private Path getAudioRecordingPath()
{
Date now = new Date();
String dailyFolder = DIRECTORY_SDF.format(now);
Path recordingDirectory = mUserPreferences.getDirectoryPreference().getDirectoryCalls()
.resolve(dailyFolder);
if(!Files.exists(recordingDirectory))
{
try
{
Files.createDirectories(recordingDirectory);
}
catch(IOException ioe)
{
LOGGING_SUPPRESSOR.error(dailyFolder, 1, "Unable to create daily calls recording " +
"folder [" + dailyFolder + "]", ioe);
return null;
}
}
int recordingNumber = sFileNameCounter++;
String fileName = FILE_NAME_SDF.format(now) + "_call_" + recordingNumber + ".wav";
return recordingDirectory.resolve(fileName);
}
/**
* Writes any buffered audio to the wave recording.
* @return true if the call was updated with the audio recording path.
*/
private boolean processAudio()
{
boolean updated = false;
if(mAudioSegment.hasAudio())
{
if(mWaveWriter == null)
{
try
{
mWaveWriter = new WaveWriter(AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO, mPath);
updated = true;
}
catch(IOException ioe)
{
LOGGER.error("Error creating wave file: " + mPath);
}
}
List<float[]> audioBuffers = mAudioSegment.getAudioBuffers();
int size = audioBuffers.size();
if(mWaveWriter != null && size > mAudioBufferPointer)
{
for(int x = mAudioBufferPointer; x < audioBuffers.size(); x++)
{
try
{
mWaveWriter.writeData(ConversionUtils.convertToSigned16BitSamples(audioBuffers.get(x)));
}
catch(IOException ioe)
{
LOGGING_SUPPRESSOR.error("Call Wave Writer", 3, "Error while recording call audio", ioe);
}
}
mAudioBufferPointer = size;
}
}
return updated;
}
/**
* Writes remaining audio and metadata to wave file and closes the recorder.
*/
public void closeAudio()
{
processAudio();
if(mWaveWriter != null)
{
Map<AudioMetadata,String> metadataMap = AudioMetadataUtils.getMetadataMap(mAudioSegment.getIdentifierCollection(),
mAudioSegment.getAliasList());
ByteBuffer listChunk = AudioMetadataUtils.getLISTChunk(metadataMap);
byte[] id3Bytes = AudioMetadataUtils.getMP3ID3(metadataMap);
ByteBuffer id3Chunk = AudioMetadataUtils.getID3Chunk(id3Bytes);
try
{
mWaveWriter.writeMetadata(listChunk, id3Chunk);
}
catch(IOException ioe)
{
LOGGING_SUPPRESSOR.error("Metadata Write Error", 3,
"Error writing call ID3 metadata to recording", ioe);
}
try
{
mWaveWriter.close();
}
catch(IOException ioe)
{
LOGGING_SUPPRESSOR.error("Recording Close Error", 3, "Error closing audio wave recording", ioe);
}
mWaveWriter = null;
}
}
/**
* Utility method to create an initial call instance from an Audio Segment. Note: this static method is used by
* an external database controller so that the call event can be stored in the database before
* @param audioSegment
* @return
*/
public static Call create(AudioSegment audioSegment)
{
if(audioSegment == null)
{
return null;
}
Call call = new Call();
call.setEventTime(audioSegment.getStartTimestamp());
Identifier decoder = audioSegment.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
Form.DECODER_TYPE, Role.ANY);
if(decoder instanceof DecoderTypeConfigurationIdentifier dtci)
{
call.setProtocol(dtci.getValue().toString());
}
Identifier systemId = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
if(systemId instanceof SystemConfigurationIdentifier sci)
{
call.setSystem(sci.getValue());
}
Identifier siteId = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SITE, Role.ANY);
if(siteId instanceof SiteConfigurationIdentifier siteci)
{
call.setSite(siteci.getValue());
}
Identifier channelId = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL, Role.ANY);
if(channelId instanceof ChannelNameConfigurationIdentifier cnci)
{
call.setChannel(cnci.getValue());
}
Identifier frequency = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.CHANNEL_FREQUENCY, Role.ANY);
if(frequency instanceof FrequencyConfigurationIdentifier fci)
{
//Convert Hertz to MegaHertz for the call value.
call.setFrequency(fci.getValue() / 1E6d);
}
updateFrom(call, audioSegment.getIdentifierCollection().getFromIdentifier(), audioSegment.getAliasList());
updateTo(call, audioSegment.getIdentifierCollection().getToIdentifier(), audioSegment.getAliasList());
updateDuration(call, audioSegment.getDuration());
updateFlags(call, audioSegment);
return call;
}
/**
* Updates the duplicate, record, stream and other call flags/info
* @param call to update
* @param audioSegment containing flags/info
* @return true if the call is updated.
*/
private static boolean updateFlags(Call call, AudioSegment audioSegment)
{
boolean updated = false;
if(call.isDuplicate() ^ audioSegment.isDuplicate())
{
call.setDuplicate(true);
updated = true;
}
if(call.isRecord() ^ audioSegment.recordAudioProperty().get())
{
call.setRecord(true);
updated = true;
}
if(call.isStream() ^ audioSegment.hasBroadcastChannels())
{
call.setStream(true);
updated = true;
}
if(call.getMonitor() != audioSegment.monitorPriorityProperty().get())
{
call.setMonitor(audioSegment.monitorPriorityProperty().get());
updated = true;
}
return updated;
}
/**
* Updates the call duration field
* @param call to update
* @param durationMs value
* @return true if the value was updated
*/
private static boolean updateDuration(Call call, long durationMs)
{
double durationSeconds = durationMs / 1000.0;
if(call.getDuration() != durationSeconds)
{
call.setDuration(durationSeconds);
return true;
}
return false;
}
/**
* Updates the FROM identifier value.
* @param call to update
* @param fromIdentifier value
* @param aliasList for alias lookups
* @return true if the value was updated
*/
private static boolean updateFrom(Call call, Identifier fromIdentifier, AliasList aliasList)
{
if(fromIdentifier == null)
{
return false;
}
if(call.getFromId() == null || call.getFromId().contentEquals(fromIdentifier.toString()))
{
call.setFromId(fromIdentifier.toString());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(fromIdentifier);
if(!aliases.isEmpty())
{
call.setFromAlias(Joiner.on(";").join(aliases));
}
}
return true;
}
return false;
}
/**
* Updates the TO identifier value.
* @param call to update
* @param toIdentifier value
* @return true if the value was updated
*/
private static boolean updateTo(Call call, Identifier toIdentifier, AliasList aliasList)
{
if(toIdentifier == null)
{
return false;
}
if(call.getToId() == null || call.getToId().contentEquals(toIdentifier.toString()))
{
call.setToId(toIdentifier.toString());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(toIdentifier);
if(!aliases.isEmpty())
{
call.setToAlias(Joiner.on(";").join(aliases));
}
}
switch(toIdentifier.getForm())
{
case TALKGROUP -> call.setCallType("Talk Group");
case RADIO ->
{
LOGGER.info("Private Call detected - TO identifier: " + toIdentifier);
call.setCallType("Private");
}
case PATCH_GROUP -> call.setCallType("Patch Group");
default -> call.setCallType(toIdentifier.getForm().toString());
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,241 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelController;
import io.github.dsheirer.audio.playbackfx.IAudioPlaybackStatusListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Separator;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
/**
* Audio playback controller.
*/
public class AudioPlaybackControlView extends HBox implements IAudioPlaybackStatusListener
{
private static final int ICON_SIZE = 16;
private ObjectProperty<MediaPlayer> mMediaPlayer = new SimpleObjectProperty<>();
private ToggleButton mAutoRepeatButton;
private ToggleGroup mPlaybackGroup;
private ToggleButton mPlayButton;
private ToggleButton mPauseButton;
private ToggleButton mStopButton;
private boolean mUpdating = false;
/**
* Constructs an instance
*/
public AudioPlaybackControlView()
{
setSpacing(2);
Separator separator = new Separator(Orientation.VERTICAL);
separator.setPadding(new Insets(0, 5, 0 , 7));
getChildren().addAll(getStopButton(), getPlayButton(), getPauseButton(), getAutoRepeatButton());
mediaPlayerProperty().addListener((observable, oldValue, newValue) -> updateControls());
}
/**
* Implements the IAudioPLaybackStatusListener method to click the stop button when end of media is detected.
* @param controller where this occurred.
*/
@Override
public void endOfMedia(AudioPlaybackChannelController controller)
{
getStopButton().setSelected(true);
}
@Override
public void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus,
MediaPlayer.Status currentStatus)
{
if(currentStatus != null)
{
switch(currentStatus)
{
case UNKNOWN:
break;
case READY:
break;
case PAUSED:
mUpdating = true;
getPauseButton().setSelected(true);
mUpdating = false;
break;
case PLAYING:
mUpdating = true;
getPlayButton().setSelected(true);
mUpdating = false;
break;
case STOPPED:
mUpdating = true;
getStopButton().setSelected(true);
mUpdating = false;
if(getAutoRepeatButton().isSelected())
{
getPlayButton().setSelected(true);
}
break;
case STALLED:
case HALTED:
case DISPOSED:
System.out.println("Playback is " + currentStatus);
break;
}
}
}
/**
* Update the controls when the media player changes
*/
private void updateControls()
{
boolean disable = mediaPlayerProperty().get() == null;
getStopButton().setDisable(disable);
getPlayButton().setDisable(disable);
getPauseButton().setDisable(disable);
getAutoRepeatButton().setDisable(disable);
}
/**
* Media player property that can be bound to a media player controller.
*/
public ObjectProperty<MediaPlayer> mediaPlayerProperty()
{
return mMediaPlayer;
}
/**
* Playback toggle buttons group (play, stop, and pause)
*/
private ToggleGroup getPlaybackGroup()
{
if(mPlaybackGroup == null)
{
mPlaybackGroup = new ToggleGroup();
mPlaybackGroup.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> {
MediaPlayer mediaPlayer = mediaPlayerProperty().get();
if(!mUpdating && mediaPlayer != null)
{
if(getPlayButton().equals(selectedToggle))
{
mediaPlayer.play();
}
else if(getPauseButton().equals(selectedToggle))
{
mediaPlayer.pause();
}
else if(getStopButton().equals(selectedToggle))
{
mediaPlayer.stop();
}
}
});
}
return mPlaybackGroup;
}
/**
* Play button
*/
private ToggleButton getPlayButton()
{
if(mPlayButton == null)
{
mPlayButton = new ToggleButton();
mPlayButton.setToggleGroup(getPlaybackGroup());
mPlayButton.setTooltip(new Tooltip("Play Audio"));
mPlayButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mPlayButton.setGraphic(iconNode);
}
return mPlayButton;
}
/**
* Pause button
*/
private ToggleButton getPauseButton()
{
if(mPauseButton == null)
{
mPauseButton = new ToggleButton();
mPauseButton.setToggleGroup(getPlaybackGroup());
mPauseButton.setTooltip(new Tooltip("Pause Audio Playback"));
mPauseButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.PAUSE);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mPauseButton.setGraphic(iconNode);
}
return mPauseButton;
}
/**
* Stop button
*/
private ToggleButton getStopButton()
{
if(mStopButton == null)
{
mStopButton = new ToggleButton();
mStopButton.setToggleGroup(getPlaybackGroup());
mStopButton.setTooltip(new Tooltip("Stop Audio Playback"));
mStopButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.STOP);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mStopButton.setGraphic(iconNode);
}
return mStopButton;
}
private ToggleButton getAutoRepeatButton()
{
if(mAutoRepeatButton == null)
{
mAutoRepeatButton = new ToggleButton();
mAutoRepeatButton.setTooltip(new Tooltip("Auto-repeat audio playback"));
mAutoRepeatButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.REPEAT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mAutoRepeatButton.setGraphic(iconNode);
}
return mAutoRepeatButton;
}
}

View File

@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.audio;
package io.github.dsheirer.audio.call;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
@@ -29,6 +29,7 @@ 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 java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -36,7 +37,6 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -74,13 +74,13 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
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 long mSampleCount = 0;
private boolean mDisposing = false;
private AudioSegment mLinkedAudioSegment;
private int mTimeslot;
private List<String> mLeases = new ArrayList<>();
/**
* Constructs an instance
@@ -94,6 +94,50 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
mIdentifierCollection.setTimeslot(timeslot);
}
/**
* Adds a lease to this audio segment. This audio segment will remain alive until all leases are removed.
* @param lease to add
*/
public void addLease(String lease)
{
synchronized(mLeases)
{
if(mLeases.contains(lease))
{
mLog.warn("Attempt to obtain duplicate lease: " + lease);
}
else
{
mLeases.add(lease);
}
}
}
/**
* Removes the lease from this audio segment. Once all leases are removed, the audio segment is disposed.
* @param lease to remove
*/
public void removeLease(String lease)
{
synchronized(mLeases)
{
if(mLeases.contains(lease))
{
mLeases.remove(lease);
}
else
{
mLog.warn("Attempt to remove a non-existent lease: " + lease);
}
if(mLeases.isEmpty())
{
mLog.warn("Invoking dispose() on non-leased audio segment - last lease was: " + lease);
dispose();
}
}
}
/**
* Timeslot for this audio segment
*/
@@ -149,28 +193,6 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mComplete.get();
}
/**
* Duplicate call audio property. This flag is set to true whenever a duplicate call detection function detects
* an audio segment is a duplicate.
*/
public BooleanProperty duplicateProperty()
{
return mDuplicate;
}
/**
* 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.
*/
@@ -273,7 +295,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
* 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)
public void addIdentifiers(Collection<Identifier> identifiers)
{
for(Identifier identifier: identifiers)
{
@@ -301,24 +323,6 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
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
*/
@@ -339,34 +343,6 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio;
package io.github.dsheirer.audio.call;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
@@ -41,11 +41,8 @@ public class AudioSegmentBroadcaster<T extends AudioSegment> extends Broadcaster
{
for(Listener<T> listener : getListeners())
{
audioSegment.incrementConsumerCount();
audioSegment.addLease(listener.getClass().toString());
listener.receive(audioSegment);
}
//Decrement consumer counter for this broadcaster
audioSegment.decrementConsumerCount();
}
}

View File

@@ -0,0 +1,495 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.SequenceGenerator;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import javafx.application.Platform;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
* Call database entity.
*/
@Entity
@Table(name="calls", schema="sdrtrunk")
public class Call
{
public static final String COLUMN_CALL_TYPE = "callType";
public static final String COLUMN_CHANNEL = "channel";
public static final String COLUMN_DUPLICATE = "duplicate";
public static final String COLUMN_DURATION = "duration";
public static final String COLUMN_EVENT_TIME = "eventTime";
public static final String COLUMN_FILE = "file";
public static final String COLUMN_FREQUENCY = "frequency";
public static final String COLUMN_FROM_ALIAS = "fromAlias";
public static final String COLUMN_FROM_ID = "fromId";
public static final String COLUMN_ID = "id";
public static final String COLUMN_MONITOR = "monitor";
public static final String COLUMN_PROTOCOL = "protocol";
public static final String COLUMN_RECORD = "record";
public static final String COLUMN_STREAM = "stream";
public static final String COLUMN_TO_ALIAS = "toAlias";
public static final String COLUMN_TO_ID = "toId";
public static final String COLUMN_SITE = "site";
public static final String COLUMN_SYSTEM = "system";
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "id_generator")
@SequenceGenerator(name = "id_generator", allocationSize = 1)
@Column(name=COLUMN_ID)
private long id;
@Column(name=COLUMN_CALL_TYPE)
private String callType;
@Column(name=COLUMN_CHANNEL)
private String channel;
@Column(name=COLUMN_DUPLICATE)
private boolean duplicate;
@Column(name=COLUMN_DURATION)
private double duration;
@Column(name=COLUMN_EVENT_TIME)
private long eventTime;
@Column(name=COLUMN_FILE)
private String file;
@Column(name=COLUMN_FREQUENCY)
private double frequency;
@Column(name=COLUMN_FROM_ALIAS)
private String fromAlias;
@Column(name=COLUMN_FROM_ID)
private String fromId;
@Column(name=COLUMN_MONITOR)
private int monitor;
@Column(name=COLUMN_PROTOCOL)
private String protocol;
@Column(name=COLUMN_RECORD)
private boolean record;
@Column(name=COLUMN_SITE)
private String site;
@Column(name=COLUMN_STREAM)
private boolean stream;
@Column(name=COLUMN_SYSTEM)
private String system;
@Column(name=COLUMN_TO_ALIAS)
private String toAlias;
@Column(name=COLUMN_TO_ID)
private String toId;
@Transient
private boolean mComplete = true;
@Transient
private LongProperty mLastUpdated = new SimpleLongProperty();
@Transient
private StringProperty mPlaybackChannel = new SimpleStringProperty();
/**
* Constructs an instance
*/
public Call()
{
}
/**
* Record ID, auto-assigned by the database.
* @return id
*/
public long getId()
{
return id;
}
/**
* Timestamp for the event
* @return timestamp in milliseconds since epoch
*/
public long getEventTime()
{
return eventTime;
}
/**
* Sets the timestamp
* @param eventTime in ms.
*/
public void setEventTime(long eventTime)
{
this.eventTime = eventTime;
}
/**
* From identifier
* @return from ID
*/
public String getFromId()
{
return fromId;
}
/**
* Sets the from identifier
* @param fromId ID
*/
public void setFromId(String fromId)
{
this.fromId = fromId;
updateTimestamp();
}
/**
* From alias
* @return from alias
*/
public String getFromAlias()
{
return fromAlias;
}
/**
* Sets the from alias value.
* @param fromAlias value
*/
public void setFromAlias(String fromAlias)
{
this.fromAlias = fromAlias;
updateTimestamp();
}
/**
* To identifier
* @return to
*/
public String getToId()
{
return toId;
}
/**
* Sets the to identifier
* @param toId value
*/
public void setToId(String toId)
{
this.toId = toId;
updateTimestamp();
}
/**
* To alias value
* @return alias value
*/
public String getToAlias()
{
return toAlias;
}
/**
* Sets the to alias value.
* @param toAlias value
*/
public void setToAlias(String toAlias)
{
this.toAlias = toAlias;
updateTimestamp();
}
/**
* System name
* @return name
*/
public String getSystem()
{
return system;
}
/**
* Sets the system name
* @param system name
*/
public void setSystem(String system)
{
this.system = system;
}
/**
* Site name
* @return site
*/
public String getSite()
{
return site;
}
/**
* Sets the site name
* @param site name
*/
public void setSite(String site)
{
this.site = site;
}
/**
* Channel name
* @return channel
*/
public String getChannel()
{
return channel;
}
/**
* Sets the channel name
* @param channel name
*/
public void setChannel(String channel)
{
this.channel = channel;
}
/**
* Frequency for the channel
* @return frequency.
*/
public double getFrequency()
{
return frequency;
}
/**
* Sets the frequency value for the channel.
* @param frequency value.
*/
public void setFrequency(double frequency)
{
this.frequency = frequency;
}
/**
* File path for the call
* @return file path
*/
public String getFile()
{
return file;
}
/**
* Sets the file path for the call.
* @param file path
*/
public void setFile(String file)
{
this.file = file;
}
/**
* Duration of the call in seconds
* @return duration seconds
*/
public double getDuration()
{
return duration;
}
/**
* Sets the call duration.
* @param duration value in seconds
*/
public void setDuration(double duration)
{
this.duration = duration;
updateTimestamp();
}
/**
* Radio protocol
* @return protocol
*/
public String getProtocol()
{
return protocol;
}
/**
* Sets the radio protocol
* @param protocol for the call
*/
public void setProtocol(String protocol)
{
this.protocol = protocol;
}
/**
* Indicates if this call is complete and will receive no further updates.
* @return true if complete.
*/
public boolean isComplete()
{
return mComplete;
}
/**
* Sets the complete status for this call.
* @param complete true if complete
*/
public void setComplete(boolean complete)
{
mComplete = complete;
updateTimestamp();
}
/**
* Call type
* @return call type
*/
public String getCallType()
{
return callType;
}
/**
* Sets the call type
* @param callType of call
*/
public void setCallType(String callType)
{
this.callType = callType;
}
/**
* Indicates if this call was flagged as a duplicate
* @return true if duplicate
*/
public boolean isDuplicate()
{
return duplicate;
}
/**
* Sets the duplicate flag for this call.
* @param duplicate flag
*/
public void setDuplicate(boolean duplicate)
{
this.duplicate = duplicate;
}
/**
* Monitor/listen priority
* @return priority where a -1 value is do not monitor.
*/
public int getMonitor()
{
return monitor;
}
/**
* Sets the monitor/listen priority
* @param monitor priority
*/
public void setMonitor(int monitor)
{
this.monitor = monitor;
}
/**
* Indicates if the call is recordable/retainable.
* @return is recordable
*/
public boolean isRecord()
{
return record;
}
/**
* Sets the record/retain flag.
* @param record flag
*/
public void setRecord(boolean record)
{
this.record = record;
}
/**
* Indicates if the call is streamable.
* @return streamable
*/
public boolean isStream()
{
return stream;
}
/**
* Sets the stream flag
* @param stream flag
*/
public void setStream(boolean stream)
{
this.stream = stream;
}
/**
* Updates the last updated timestamp.
*/
private void updateTimestamp()
{
if(Platform.isFxApplicationThread())
{
lastUpdatedProperty().set(System.currentTimeMillis());
}
else
{
Platform.runLater(() -> lastUpdatedProperty().set(System.currentTimeMillis()));
}
}
/**
* Last updated timestamp. This value is updated each time any attribute in the call record is updated so that
* consumer processes can monitor this property to be notified of updates.
* @return timestamp of last update in milliseconds.
*/
public LongProperty lastUpdatedProperty()
{
return mLastUpdated;
}
/**
* Current playback channel for this call.
* @return current playback channel or null if this call is not being played back.
*/
public StringProperty playbackChannelProperty()
{
return mPlaybackChannel;
}
@Override
public boolean equals(Object obj)
{
return obj instanceof Call c && getId() == c.getId();
}
}

View File

@@ -0,0 +1,63 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
/**
* Calls database repository
*/
public interface CallRepository extends JpaRepository<Call,Long>
{
@Query("SELECT t FROM Call t WHERE t.toId like ?1 OR t.toAlias like ?1 OR t.fromId like ?1 OR t.fromAlias like ?1")
Page<Call> findByAnyIdWithPagination(String value, Pageable pageable);
@Query("SELECT t FROM Call t WHERE t.toId like ?1 OR t.toAlias like ?1")
Page<Call> findByToWithPagination(String value, Pageable pageable);
@Query("SELECT t FROM Call t WHERE t.fromId like ?1 OR t.fromAlias like ?1")
Page<Call> findByFromWithPagination(String value, Pageable pageable);
@Query("SELECT t FROM Call t WHERE t.callType like ?1")
Page<Call> findByCallTypeWithPagination(String value, Pageable pageable);
@Query("SELECT t FROM Call t WHERE t.channel like ?1")
Page<Call> findByChannelWithPagination(String value, Pageable pageable);
@Query("SELECT t FROM Call t WHERE t.protocol like ?1")
Page<Call> findByProtocolWithPagination(String value, Pageable pageable);
@Query("SELECT t FROM Call t WHERE t.site like ?1")
Page<Call> findBySiteWithPagination(String value, Pageable pageable);
@Query("SELECT t FROM Call t WHERE t.system like ?1")
Page<Call> findBySystemWithPagination(String value, Pageable pageable);
/**
* Find all calls between the timestamp range arguments.
* @param start timestamp
* @param end timestamp
* @return pageable set of calls.
*/
Page<Call> findCallsByEventTimeBetweenOrderByEventTime(long start, long end, Pageable pageable);
}

View File

@@ -0,0 +1,971 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.audio.AudioManager;
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelController;
import io.github.dsheirer.audio.playbackfx.AudioPlaybackController;
import io.github.dsheirer.audio.playbackfx.PlaybackMode;
import io.github.dsheirer.gui.control.TimestampTableCellFactory;
import io.github.dsheirer.gui.javafx.table.TableColumnEditor;
import io.github.dsheirer.gui.javafx.table.TableViewColumnController;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import javafx.application.Platform;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.Separator;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.util.Callback;
import jiconfont.icons.elusive.Elusive;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
/**
* Audio call events playback and audio event database control/view.
*/
public class CallView extends VBox implements IPageRequestListener
{
private static final Logger LOGGER = LoggerFactory.getLogger(CallView.class);
private static final Sort DEFAULT_SORT = Sort.by("eventTime").descending();
@Resource
private AudioManager mAudioManager;
@Resource
private CallRepository mCallRepository;
@Resource
private AudioPlaybackController mAudioPlaybackController;
private CallEventListener mCallEventListener = new CallEventListener();
private SortOrderListener mSortOrderListener = new SortOrderListener();
private ObservableList<Call> mCalls = FXCollections.observableArrayList();
private TableView<Call> mCallTableView;
private TableViewColumnController mCallTableViewMonitor;
private ProgressIndicator mPlaceholderProgressIndicator;
private HBox mSearchBox;
private HBox mPagingBox;
private TextField mSearchField;
private PagingController mPagingController;
private ComboBox<Integer> mPageSizeComboBox;
private Label mCallsCountLabel;
private LongProperty mCallCount = new SimpleLongProperty();
private Sort mSort = DEFAULT_SORT;
private ComboBox<SearchColumn> mSearchColumnComboBox;
private CheckBox mSearchExactCheckBox;
private AudioPlaybackControlView mAudioPlaybackControlView;
private Button mAutoModeLeftButton;
private Button mAutoModeRightButton;
private Button mLockModeLeftButton;
private Button mLockModeRightButton;
private TableColumnEditor mTableColumnEditor;
private SplitPane mTableSplitPane;
private Button mConfigureColumnsButton;
/**
* Constructs an instance
*/
public CallView()
{
mCallCount.addListener((observable, oldValue, newValue) -> {
getCallsCountLabel().setText("Calls: " + newValue);
int pageCount = (int)(mCallCount.get() / getPageSize());
if(mCallCount.get() % getPageSize() != 0)
{
pageCount++;
}
getPagingController().update(getPagingController().getCurrentPage(), pageCount);
});
}
/**
* Post instantiation/startup steps.
*/
@PostConstruct
public void postConstruct()
{
//Register the audio playback control view as a playback status listener with the primary audio channel controller
mAudioPlaybackController.getPrimaryController().add(getAudioPlaybackControlView());
HBox controlBox = new HBox();
controlBox.setAlignment(Pos.CENTER);
controlBox.setSpacing(2);
controlBox.setPadding(new Insets(5, 5, 5, 5));
getAudioPlaybackControlView().setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(getSearchBox(), Priority.ALWAYS);
getSearchBox().setAlignment(Pos.CENTER_RIGHT);
Separator separator1 = new Separator(Orientation.VERTICAL);
separator1.setPadding(new Insets(0, 10, 0, 10));
Separator separator2 = new Separator(Orientation.VERTICAL);
separator2.setPadding(new Insets(0, 10, 0, 10));
Label autoLabel = new Label("Auto:");
Label lockLabel = new Label("Lock:");
getSearchExactCheckBox().setPadding(new Insets(0, 10, 0, 0));
controlBox.getChildren().addAll(getAudioPlaybackControlView(), separator1, autoLabel, getAutoModeLeftButton(),
getAutoModeRightButton(), separator2, lockLabel, getLockModeLeftButton(), getLockModeRightButton(),
getSearchBox(), getConfigureColumnsButton());
getTableColumnEditor().closedProperty().addListener((observable, previouslyClosed, closed) -> {
if(closed && !previouslyClosed)
{
getTableSplitPane().getItems().remove(getTableColumnEditor());
}
});
VBox.setVgrow(getTableSplitPane(), Priority.ALWAYS);
getChildren().addAll(controlBox, getTableSplitPane(), getPagingBox());
getPagingController().setPadding(new Insets(5, 5, 5, 5));
getPagingController().update(1, 20);
//Register for call event notifications
mAudioManager.add(mCallEventListener);
getPlaceholderProgressIndicator().setVisible(true);
}
/**
* Pre-destruction or shutdown steps
*/
@PreDestroy
public void preDestroy()
{
//De-register from call event notifications
mAudioManager.remove(mCallEventListener);
}
/**
* Shows first page of results.
*/
private void showFirstPage()
{
showPage(1);
}
/**
* Sets the page to view. This implements the IPageRequestListener interface.
* @param page number to display.
*/
@Override
public void showPage(int page)
{
if(page >= 1)
{
Platform.runLater(() -> getPlaceholderProgressIndicator().setVisible(true));
final String searchTerm = getSearchField().getText();
final SearchColumn searchColumn = getSearchColumnComboBox().getSelectionModel().getSelectedItem();
final boolean wildcard = !getSearchExactCheckBox().isSelected();
//Spin the query off onto a cached thread and then place the results back onto the FX thread.
ThreadPool.CACHED.submit(() ->
{
final PageRequest pageRequest = PageRequest.of(page - 1, getPageSize(), mSort);
try
{
Page<Call> callPage = null;
if(searchTerm != null && !searchTerm.isEmpty() && searchColumn != null)
{
final String wildcardSearch = (wildcard ? "%" : "") + searchTerm + (wildcard ? "%" : "");
switch(searchColumn)
{
case ANY_ID -> callPage = mCallRepository.findByAnyIdWithPagination(wildcardSearch, pageRequest);
case CALL_TYPE -> callPage = mCallRepository.findByCallTypeWithPagination(wildcardSearch, pageRequest);
case CHANNEL -> callPage = mCallRepository.findByChannelWithPagination(wildcardSearch, pageRequest);
case FROM -> callPage = mCallRepository.findByFromWithPagination(wildcardSearch, pageRequest);
case PROTOCOL -> callPage = mCallRepository.findByProtocolWithPagination(wildcardSearch, pageRequest);
case SITE -> callPage = mCallRepository.findBySiteWithPagination(wildcardSearch, pageRequest);
case SYSTEM -> callPage = mCallRepository.findBySystemWithPagination(wildcardSearch, pageRequest);
case TO -> callPage = mCallRepository.findByToWithPagination(wildcardSearch, pageRequest);
}
}
else
{
callPage = mCallRepository.findAll(pageRequest);
}
if(callPage != null)
{
final Page<Call> finalCallPage = callPage;
Platform.runLater(() -> {
mCalls.clear();
mCalls.addAll(finalCallPage.stream().toList());
getPlaceholderProgressIndicator().setVisible(false);
getPagingController().update(finalCallPage.getNumber() + 1, finalCallPage.getTotalPages());
mCallCount.set(finalCallPage.getTotalElements());
});
}
}
catch(Throwable t)
{
LOGGER.error("Error submitting paging [" + page + "] query to database", t);
Platform.runLater(() -> {
getPlaceholderProgressIndicator().setVisible(false);
Alert alert = new Alert(Alert.AlertType.ERROR);
if(getParent() != null)
{
alert.initOwner(getParent().getScene().getWindow());
}
alert.setHeaderText("Database Error!");
alert.setContentText("Error retrieving page " + page + " from the database - reason: " + t.getMessage());
alert.showAndWait();
});
}
});
}
else
{
LOGGER.warn("Invalid page [" + page + "] number request");
}
}
/**
* Updates the sort order for the table and resets the page to page 1.
*/
private void updateSortOrder()
{
mSort = null;
for(TableColumn<Call,?> tableColumn: getCallTableView().getSortOrder())
{
Sort columnSort = Sort.by(tableColumn.getId());
if(tableColumn.getSortType() == TableColumn.SortType.DESCENDING)
{
columnSort = columnSort.descending();
}
if(mSort == null)
{
mSort = columnSort;
}
else
{
mSort = mSort.and(columnSort);
}
}
if(mSort == null)
{
mSort = DEFAULT_SORT;
}
showFirstPage();
}
/**
* Current page size
*/
private int getPageSize()
{
return getPageSizeComboBox().getValue();
}
/**
* Configure column order and visibility button
*/
private Button getConfigureColumnsButton()
{
if(mConfigureColumnsButton == null)
{
mConfigureColumnsButton = new Button();
mConfigureColumnsButton.setTooltip(new Tooltip("Configure call table column ordering and visibility"));
IconNode icon = new IconNode(FontAwesome.COG);
mConfigureColumnsButton.setGraphic(icon);
mConfigureColumnsButton.setOnAction(event -> {
//Set the split pane to expose the column configuration panel.
if(!getTableSplitPane().getItems().contains(getTableColumnEditor()))
{
getTableSplitPane().getItems().add(getTableColumnEditor());
}
getTableColumnEditor().closedProperty().set(false);
double splitWidth = getTableSplitPane().getWidth();
double scale = 0.5;
if(splitWidth > 260)
{
scale = (splitWidth - 260) / splitWidth;
}
getTableSplitPane().setDividerPosition(0, scale);
});
}
return mConfigureColumnsButton;
}
/**
* Split pane for call table and table column editor
*/
private SplitPane getTableSplitPane()
{
if(mTableSplitPane == null)
{
mTableSplitPane = new SplitPane(getCallTableView());
}
return mTableSplitPane;
}
/**
* Table column order/visibility editor
*/
private TableColumnEditor getTableColumnEditor()
{
if(mTableColumnEditor == null)
{
mTableColumnEditor = new TableColumnEditor(getCallTableView(), mCallTableViewMonitor);
}
return mTableColumnEditor;
}
/**
* Sets playback mode to auto
*/
private Button getAutoModeLeftButton()
{
if(mAutoModeLeftButton == null)
{
mAutoModeLeftButton = new Button("Left");
mAutoModeLeftButton.setTooltip(new Tooltip("Set left audio channel to auto playback mode"));
AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
mAutoModeLeftButton.disableProperty().bind(left.playbackModeProperty().isEqualTo(PlaybackMode.AUTO));
mAutoModeLeftButton.setOnAction(event -> {
final AudioPlaybackChannelController controller = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
if(controller != null)
{
getCallTableView().getSelectionModel().clearSelection();
controller.auto();
}
});
}
return mAutoModeLeftButton;
}
/**
* Sets playback mode to auto
*/
private Button getAutoModeRightButton()
{
if(mAutoModeRightButton == null)
{
mAutoModeRightButton = new Button("Right");
mAutoModeRightButton.setTooltip(new Tooltip("Set right audio channel to auto playback mode"));
AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
mAutoModeRightButton.disableProperty().bind(right.playbackModeProperty().isEqualTo(PlaybackMode.AUTO));
mAutoModeRightButton.setOnAction(event -> {
final AudioPlaybackChannelController controller = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
if(controller != null)
{
getCallTableView().getSelectionModel().clearSelection();
controller.auto();
}
});
}
return mAutoModeRightButton;
}
/**
* Sets playback mode to auto - locked to the currently selected TO identifier to the left channel
*/
private Button getLockModeLeftButton()
{
if(mLockModeLeftButton == null)
{
mLockModeLeftButton = new Button("Left");
mLockModeLeftButton.setTooltip(new Tooltip("Lock the left audio channel for auto playback of selected call talkgroup"));
mLockModeLeftButton.setDisable(true);
mLockModeLeftButton.setOnAction(event -> {
final AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
if(left != null)
{
final Call selected = getCallTableView().getSelectionModel().getSelectedItem();
getCallTableView().getSelectionModel().clearSelection();
getLockModeLeftButton().setDisable(true);
if(selected != null)
{
//If the right controller is locked for this same call, reset it to auto so that only 1 is locked
final AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
if(right.isLockedFor(selected))
{
right.auto();
}
left.lockAutoTo(selected.getToId(), selected.getSystem());
}
else
{
left.auto();
}
}
});
}
return mLockModeLeftButton;
}
/**
* Sets playback mode to auto - locked to the currently selected TO identifier to the left channel
*/
private Button getLockModeRightButton()
{
if(mLockModeRightButton == null)
{
mLockModeRightButton = new Button("Right");
mLockModeRightButton.setTooltip(new Tooltip("Lock the left audio channel for auto playback of selected call talkgroup"));
mLockModeRightButton.setDisable(true);
mLockModeRightButton.setOnAction(event -> {
final AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
if(right != null)
{
final Call selected = getCallTableView().getSelectionModel().getSelectedItem();
getCallTableView().getSelectionModel().clearSelection();
getLockModeRightButton().setDisable(true);
if(selected != null)
{
//If the left controller is locked for this same call, reset it to auto so that only 1 is locked
final AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
if(left.isLockedFor(selected))
{
left.auto();
}
right.lockAutoTo(selected.getToId(), selected.getSystem());
}
else
{
right.auto();
}
}
});
}
return mLockModeRightButton;
}
/**
* Audio playback control view.
*/
private AudioPlaybackControlView getAudioPlaybackControlView()
{
if(mAudioPlaybackControlView == null)
{
mAudioPlaybackControlView = new AudioPlaybackControlView();
}
return mAudioPlaybackControlView;
}
/**
* Calls label to display the number of calls in the database.
*/
private Label getCallsCountLabel()
{
if(mCallsCountLabel == null)
{
mCallsCountLabel = new Label("Calls: 0");
}
return mCallsCountLabel;
}
/**
* Toggles wildcard searching
* @return true for exact match or false for wildcard/fuzzy matching.
*/
private CheckBox getSearchExactCheckBox()
{
if(mSearchExactCheckBox == null)
{
mSearchExactCheckBox = new CheckBox("Exact Match");
mSearchExactCheckBox.setTooltip(new Tooltip("Toggle exact matching (checked) or wildcard/fuzzy " +
"matching (unchecked)"));
mSearchExactCheckBox.selectedProperty().addListener((observable, oldValue, newValue) -> showFirstPage());
}
return mSearchExactCheckBox;
}
/**
* Page size selection combo box.
*
* Note: when the user changes the page size, we always request page 1 again.
*/
private ComboBox<Integer> getPageSizeComboBox()
{
if(mPageSizeComboBox == null)
{
ObservableList<Integer> pageSizes = FXCollections.observableArrayList();
pageSizes.addAll(25, 50, 100, 200);
mPageSizeComboBox = new ComboBox<>(pageSizes);
mPageSizeComboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> showFirstPage());
mPageSizeComboBox.setTooltip(new Tooltip("Select the page size"));
mPageSizeComboBox.getSelectionModel().select(0);
}
return mPageSizeComboBox;
}
/**
* Page size selection combo box.
*
* Note: when the user changes the page size, we always request page 1 again.
*/
private ComboBox<SearchColumn> getSearchColumnComboBox()
{
if(mSearchColumnComboBox == null)
{
mSearchColumnComboBox = new ComboBox<>(FXCollections.observableArrayList(SearchColumn.values()));
mSearchColumnComboBox.getSelectionModel().select(SearchColumn.ANY_ID);
mSearchColumnComboBox.setTooltip(new Tooltip("Select the field(s) to search against"));
}
return mSearchColumnComboBox;
}
/**
* Paging controls panel
*/
private HBox getPagingBox()
{
if(mPagingBox == null)
{
mPagingBox = new HBox();
mPagingBox.setAlignment(Pos.CENTER_LEFT);
mPagingBox.setSpacing(3);
Label pageSizeLabel = new Label("Page Size:");
pageSizeLabel.setPadding(new Insets(0, 2, 0, 10));
getCallsCountLabel().setPadding(new Insets(0, 0, 0, 10));
mPagingBox.getChildren().addAll(getPagingController(), pageSizeLabel, getPageSizeComboBox(), getCallsCountLabel());
}
return mPagingBox;
}
/**
* Call table view
* @return
*/
private TableView<Call> getCallTableView()
{
if(mCallTableView == null)
{
mCallTableView = new TableView<>(mCalls);
mCallTableView.setMaxWidth(Double.MAX_VALUE);
TableColumn playbackColumn = new TableColumn();
playbackColumn.setSortable(false);
playbackColumn.setId("playback");
playbackColumn.sortTypeProperty().addListener(mSortOrderListener);
IconNode iconNode = new IconNode(Elusive.VOLUME_UP);
iconNode.setIconSize(16);
iconNode.setFill(Color.BLACK);
playbackColumn.setGraphic(iconNode);
playbackColumn.setCellValueFactory(new PropertyValueFactory<>("playbackChannel"));
playbackColumn.setPrefWidth(40);
TableColumn eventTimeColumn = new TableColumn();
eventTimeColumn.setId("eventTime");
eventTimeColumn.sortTypeProperty().addListener(mSortOrderListener);
eventTimeColumn.setText("Date/Time");
eventTimeColumn.setCellValueFactory(new PropertyValueFactory<Call, Long>(Call.COLUMN_EVENT_TIME));
eventTimeColumn.setCellFactory(new TimestampTableCellFactory<Call>());
eventTimeColumn.setPrefWidth(160);
TableColumn durationColumn = new TableColumn();
durationColumn.setId("duration");
durationColumn.sortTypeProperty().addListener(mSortOrderListener);
durationColumn.setText("Duration");
durationColumn.setCellValueFactory(new PropertyValueFactory<Call, Double>(Call.COLUMN_DURATION));
durationColumn.setPrefWidth(70);
TableColumn callTypeColumn = new TableColumn();
callTypeColumn.setId("callType");
callTypeColumn.sortTypeProperty().addListener(mSortOrderListener);
callTypeColumn.setText("Type");
callTypeColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_CALL_TYPE));
callTypeColumn.setPrefWidth(85);
TableColumn toIdColumn = new TableColumn();
toIdColumn.setId("toId");
toIdColumn.sortTypeProperty().addListener(mSortOrderListener);
toIdColumn.setText("To");
toIdColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_TO_ID));
toIdColumn.setPrefWidth(80);
TableColumn toAliasColumn = new TableColumn();
toAliasColumn.setId("toAlias");
toAliasColumn.sortTypeProperty().addListener(mSortOrderListener);
toAliasColumn.setText("To Alias");
toAliasColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_TO_ALIAS));
toAliasColumn.setPrefWidth(100);
TableColumn fromIdColumn = new TableColumn();
fromIdColumn.setId("fromId");
fromIdColumn.sortTypeProperty().addListener(mSortOrderListener);
fromIdColumn.setText("From");
fromIdColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_FROM_ID));
fromIdColumn.setPrefWidth(70);
TableColumn fromAliasColumn = new TableColumn();
fromAliasColumn.setId("fromAlias");
fromAliasColumn.sortTypeProperty().addListener(mSortOrderListener);
fromAliasColumn.setText("From Alias");
fromAliasColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_FROM_ALIAS));
fromAliasColumn.setPrefWidth(100);
TableColumn protocolColumn = new TableColumn();
protocolColumn.setId("protocol");
protocolColumn.sortTypeProperty().addListener(mSortOrderListener);
protocolColumn.setText("Protocol");
protocolColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_PROTOCOL));
protocolColumn.setPrefWidth(90);
TableColumn frequencyColumn = new TableColumn();
frequencyColumn.setId("frequency");
frequencyColumn.sortTypeProperty().addListener(mSortOrderListener);
frequencyColumn.setText("Frequency");
frequencyColumn.setCellValueFactory(new PropertyValueFactory<Call, Double>(Call.COLUMN_FREQUENCY));
frequencyColumn.setPrefWidth(80);
TableColumn systemColumn = new TableColumn();
systemColumn.setId("system");
systemColumn.sortTypeProperty().addListener(mSortOrderListener);
systemColumn.setText("System");
systemColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_SYSTEM));
systemColumn.setPrefWidth(80);
TableColumn siteColumn = new TableColumn();
siteColumn.setVisible(false);
siteColumn.setId("mSite");
siteColumn.sortTypeProperty().addListener(mSortOrderListener);
siteColumn.setText("Site");
siteColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_SITE));
siteColumn.setPrefWidth(80);
TableColumn channelColumn = new TableColumn();
channelColumn.setVisible(false);
channelColumn.setId("channel");
channelColumn.sortTypeProperty().addListener(mSortOrderListener);
channelColumn.setText("Channel");
channelColumn.setCellValueFactory(new PropertyValueFactory<Call, String>(Call.COLUMN_CHANNEL));
channelColumn.setPrefWidth(80);
TableColumn duplicateColumn = new TableColumn();
duplicateColumn.setVisible(false);
duplicateColumn.setId("duplicate");
duplicateColumn.sortTypeProperty().addListener(mSortOrderListener);
duplicateColumn.setText("Duplicate");
duplicateColumn.setCellValueFactory(new PropertyValueFactory<Call, Boolean>(Call.COLUMN_DUPLICATE));
duplicateColumn.setPrefWidth(80);
TableColumn recordColumn = new TableColumn();
recordColumn.setVisible(false);
recordColumn.setId("record");
recordColumn.sortTypeProperty().addListener(mSortOrderListener);
recordColumn.setText("Record");
recordColumn.setCellValueFactory(new PropertyValueFactory<Call, Boolean>(Call.COLUMN_RECORD));
recordColumn.setPrefWidth(80);
TableColumn streamColumn = new TableColumn();
streamColumn.setVisible(false);
streamColumn.setId("stream");
streamColumn.sortTypeProperty().addListener(mSortOrderListener);
streamColumn.setText("Stream");
streamColumn.setCellValueFactory(new PropertyValueFactory<Call, Boolean>(Call.COLUMN_STREAM));
streamColumn.setPrefWidth(80);
TableColumn monitorColumn = new TableColumn();
monitorColumn.setVisible(false);
monitorColumn.setId("monitor");
monitorColumn.sortTypeProperty().addListener(mSortOrderListener);
monitorColumn.setText("Monitor");
monitorColumn.setCellValueFactory(new PropertyValueFactory<Call, Integer>(Call.COLUMN_MONITOR));
monitorColumn.setCellFactory(new PriorityCellFactory());
monitorColumn.setPrefWidth(80);
mCallTableView.getSortOrder().addListener(mSortOrderListener);
mCallTableView.getColumns().addAll(playbackColumn, eventTimeColumn, durationColumn, callTypeColumn,
toIdColumn, toAliasColumn, fromIdColumn, fromAliasColumn, protocolColumn, frequencyColumn,
systemColumn, siteColumn, channelColumn, duplicateColumn, recordColumn, streamColumn, monitorColumn);
mCallTableView.setPlaceholder(getPlaceholderProgressIndicator());
mCallTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, selectedCall) -> {
if(selectedCall != null)
{
mAudioPlaybackController.replay(MixerChannel.LEFT, selectedCall);
}
else if(oldValue != null && selectedCall == null)
{
//We were in replay but now we don't have a selected call
mAudioPlaybackController.auto(MixerChannel.LEFT, null, null);
}
AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
getLockModeLeftButton().setDisable(selectedCall == null || left.isLockedFor(selectedCall));
getLockModeRightButton().setDisable(selectedCall == null || right.isLockedFor(selectedCall));
});
getPlaceholderProgressIndicator().setVisible(true);
//Bind the playback control view to the primary audio channel playback controller
getAudioPlaybackControlView().mediaPlayerProperty()
.bind(mAudioPlaybackController.getPrimaryController().mediaPlayerProperty());
mCallTableViewMonitor = new TableViewColumnController(mCallTableView, "call_table_view");
}
return mCallTableView;
}
/**
* Paging controller
*/
public PagingController getPagingController()
{
if(mPagingController == null)
{
mPagingController = new PagingController(this::showPage);
}
return mPagingController;
}
/**
* Progress indicator to convey data loading status when the table content is being updated.
* @return progress indicator
*/
public ProgressIndicator getPlaceholderProgressIndicator()
{
if(mPlaceholderProgressIndicator == null)
{
mPlaceholderProgressIndicator = new ProgressIndicator();
mPlaceholderProgressIndicator.setVisible(false);
mPlaceholderProgressIndicator.setProgress(-1); //always spinning
}
return mPlaceholderProgressIndicator;
}
private HBox getSearchBox()
{
if(mSearchBox == null)
{
mSearchBox = new HBox();
mSearchBox.setAlignment(Pos.CENTER_LEFT);
mSearchBox.setSpacing(5);
Label searchLabel = new Label("Search:");
searchLabel.setAlignment(Pos.CENTER_RIGHT);
mSearchBox.getChildren().addAll(searchLabel, getSearchField(), getSearchColumnComboBox(), getSearchExactCheckBox());
}
return mSearchBox;
}
private TextField getSearchField()
{
if(mSearchField == null)
{
mSearchField = TextFields.createClearableTextField();
mSearchField.setTooltip(new Tooltip("Type a value to search against the selected database field(s)"));
mSearchField.textProperty().addListener((observable, oldValue, newValue) -> {
showFirstPage();
});
}
return mSearchField;
}
/**
* Monitors the calls table view for sort column and sort order changes to refresh the call event records.
*/
private class SortOrderListener implements ListChangeListener<TableColumn<Call, ?>>, ChangeListener
{
@Override
public void onChanged(Change<? extends TableColumn<Call, ?>> c)
{
updateSortOrder();
}
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue)
{
updateSortOrder();
}
}
/**
* Handles call events from the AudioManager.
*/
private class CallEventListener implements ICallEventListener
{
@Override
public void added(Call call)
{
Platform.runLater(() -> {
//Only update the calls table if we're viewing page 1
if(getPagingController().getCurrentPage() == 1)
{
if(mCalls.size() == getPageSize())
{
mCalls.remove(mCalls.size() - 1);
}
mCalls.add(0, call);
getCallTableView().sort();
//If the user hasn't selected an item, scroll to the new item.
if(getCallTableView().getSelectionModel().getSelectedItem() != null)
{
getCallTableView().scrollTo(call);
}
}
mCallCount.set(mCallCount.get() + 1);
});
}
@Override
public void updated(Call call)
{
Platform.runLater(() -> {
if(mCalls.contains(call))
{
mCalls.set(mCalls.indexOf(call), call);
}
});
}
@Override
public void completed(Call call, AudioSegment audioSegment)
{
Platform.runLater(() -> {
if(mCalls.contains(call))
{
mCalls.set(mCalls.indexOf(call), call);
}
});
audioSegment.removeLease(getClass().toString());
}
@Override
public void deleted(Call call)
{
Platform.runLater(() -> {
if(mCalls.contains(call))
{
mCalls.remove(call);
}
mCallCount.set(mCallCount.get() - 1);
});
}
}
public class PriorityCellFactory implements Callback<TableColumn<Alias, Integer>, TableCell<Alias, Integer>>
{
@Override
public TableCell<Alias, Integer> call(TableColumn<Alias, Integer> param)
{
TableCell tableCell = new TableCell<Alias, Integer>()
{
@Override
protected void updateItem(Integer item, boolean empty)
{
if(empty)
{
setText(null);
setGraphic(null);
}
else if(item == io.github.dsheirer.alias.id.priority.Priority.DO_NOT_MONITOR)
{
setText("Mute");
final IconNode iconNode = new IconNode(FontAwesome.VOLUME_OFF);
iconNode.setIconSize(20);
iconNode.setFill(Color.RED);
setGraphic(iconNode);
}
else if(item == io.github.dsheirer.alias.id.priority.Priority.DEFAULT_PRIORITY)
{
setText("Default");
final IconNode iconNode = new IconNode(FontAwesome.VOLUME_UP);
iconNode.setIconSize(20);
iconNode.setFill(Color.GREEN);
setGraphic(iconNode);
}
else
{
setText(item.toString());
final IconNode iconNode = new IconNode(FontAwesome.VOLUME_UP);
iconNode.setIconSize(20);
iconNode.setFill(Color.GREEN);
setGraphic(iconNode);
}
}
};
return tableCell;
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
/**
* Completed call record
* @param call associated with the audio segment.
* @param audioSegment associated with the call.
*/
public record CompletedCall(Call call, AudioSegment audioSegment)
{
}

View File

@@ -0,0 +1,51 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
/**
* Call event listener interface
*/
public interface ICallEventListener
{
/**
* Call added event.
* @param call that was added
*/
void added(Call call);
/**
* Call updated event.
* @param call that was updated
*/
void updated(Call call);
/**
* Call completed event. This indicates that the call was completed and no further updates will be available.
* @param call that was completed.
* @param audioSegment for the call
*/
void completed(Call call, AudioSegment audioSegment);
/**
* Call that was deleted (age-off) from the database.
* @param call that was deleted.
*/
void deleted(Call call);
}

View File

@@ -0,0 +1,32 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
/**
* Listener interface for receiving paging requests from user initiated actions.
*/
public interface IPageRequestListener
{
/**
* Indicates the user requests to display a page number
* @param page number to display.
*/
void showPage(int page);
}

View File

@@ -0,0 +1,236 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
import java.text.NumberFormat;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
/**
* Implements a set of paging controls.
*/
public class PagingController extends HBox
{
private static final int ICON_SIZE = 20;
private NumberFormat mNumberFormat = NumberFormat.getIntegerInstance();
private Button mFirstButton;
private Button mLastButton;
private Button mNextButton;
private Button mPreviousButton;
private Label mPageLabel;
private int mPageCount = 1;
private int mCurrentPage = 1;
private IPageRequestListener mPageRequestListener;
/**
* Constructs an instance
* @param listener to receive page requests
*/
public PagingController(IPageRequestListener listener)
{
mPageRequestListener = listener;
setAlignment(Pos.CENTER);
setSpacing(0);
getChildren().addAll(getFirstButton(), getPreviousButton(), getPageLabel(), getNextButton(), getLastButton());
update(1, 1);
}
/**
* Updates the current state of this paging controller.
* @param currentPage number to display.
* @param pageCount the total number of available pages.
*/
public void update(int currentPage, int pageCount)
{
mPageCount = pageCount;
mCurrentPage = currentPage;
getFirstButton().setDisable(mCurrentPage <= 1);
getFirstButton().setTooltip(mCurrentPage <= 1 ? null : new Tooltip("Move to page 1"));
getPreviousButton().setDisable(mCurrentPage <= 1);
getPreviousButton().setTooltip(mCurrentPage <= 1 ? null : new Tooltip("Move to page " + (mCurrentPage - 1)));
getNextButton().setDisable(mCurrentPage >= mPageCount);
getNextButton().setTooltip(mCurrentPage >= mPageCount ? null : new Tooltip("Move to page " + (mCurrentPage + 1)));
getLastButton().setDisable(mCurrentPage >= mPageCount);
getLastButton().setTooltip(mCurrentPage >= mPageCount ? null : new Tooltip("Move to page " + mPageCount));
getPageLabel().setText("Page " + mNumberFormat.format(mCurrentPage) + " of " + mNumberFormat.format(mPageCount));
}
/**
* Total pages available
* @return page count
*/
public int getPageCount()
{
return mPageCount;
}
/**
* Current page number
* @return current page.
*/
public int getCurrentPage()
{
return mCurrentPage;
}
/**
* Notifies the listener that the user requests the page number
* @param page number requested
*/
private void request(int page)
{
if(mPageRequestListener != null)
{
mPageRequestListener.showPage(page);
}
}
/**
* Next page
*/
private void next()
{
request(mCurrentPage + 1);
}
/**
* Previous page
*/
private void previous()
{
request(mCurrentPage - 1);
}
/**
* First page
*/
private void first()
{
request(1);
}
/**
* Last page
*/
private void last()
{
request(mPageCount);
}
/**
* Current page status label.
*/
private Label getPageLabel()
{
if(mPageLabel == null)
{
mPageLabel = new Label("Page 1 of 1");
mPageLabel.setTooltip(new Tooltip("Current Page and Total Page Count"));
mPageLabel.setAlignment(Pos.CENTER);
mPageLabel.setMaxWidth(Double.MAX_VALUE);
mPageLabel.setPadding(new Insets(0, 20, 0, 20));
}
return mPageLabel;
}
/**
* First page button.
*/
private Button getFirstButton()
{
if(mFirstButton == null)
{
mFirstButton = new Button();
mFirstButton.setTooltip(new Tooltip("Go To First Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_LEFT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mFirstButton.setGraphic(iconNode);
mFirstButton.setOnAction(event -> first());
}
return mFirstButton;
}
/**
* Last page button
*/
private Button getLastButton()
{
if(mLastButton == null)
{
mLastButton = new Button();
mLastButton.setTooltip(new Tooltip("Go To Last Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_RIGHT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mLastButton.setGraphic(iconNode);
mLastButton.setOnAction(event -> last());
}
return mLastButton;
}
/**
* Next page button
*/
private Button getNextButton()
{
if(mNextButton == null)
{
mNextButton = new Button();
mNextButton.setTooltip(new Tooltip("Go To Next Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_RIGHT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mNextButton.setGraphic(iconNode);
mNextButton.setOnAction(event -> next());
}
return mNextButton;
}
/**
* Previous page button.
*/
private Button getPreviousButton()
{
if(mPreviousButton == null)
{
mPreviousButton = new Button();
mPreviousButton.setTooltip(new Tooltip("Go To Previous Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_LEFT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mPreviousButton.setGraphic(iconNode);
mPreviousButton.setOnAction(event -> previous());
}
return mPreviousButton;
}
}

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
/**
* Search columns
*/
public enum SearchColumn
{
ANY_ID("Any ID"),
TO("To / Alias"),
FROM("From / Alias"),
CHANNEL("Channel"),
PROTOCOL("Protocol"),
SITE("Site"),
SYSTEM("System"),
CALL_TYPE("Type");
private String mLabel;
/**
* Constructs an instance
* @param label to display
*/
private SearchColumn(String label)
{
mLabel = label;
}
/**
* Display label
*/
public String toString()
{
return mLabel;
}
}

View File

@@ -19,16 +19,17 @@
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.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -42,28 +43,38 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
ISquelchStateListener
ISquelchStateListener, IPreferenceUpdateListener
{
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private IAudioCodec mAudioCodec;
private final UserPreferences mUserPreferences;
private UserPreferences mUserPreferences;
private static Class sLoadedJmbeAudioConverterClass;
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
{
super(aliasList, timeslot, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
mUserPreferences = userPreferences;
MyEventBus.getGlobalEventBus().register(this);
loadConverter();
}
@PostConstruct
public void init()
{
mUserPreferences.addUpdateListener(this);
}
@PreDestroy
public void destroy()
{
mUserPreferences.removeUpdateListener(this);
}
@Override
public void dispose()
{
super.dispose();
MyEventBus.getGlobalEventBus().unregister(this);
mAudioCodec = null;
}
@@ -91,7 +102,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
*
* @param preferenceType that was updated
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.JMBE_LIBRARY)

View File

@@ -21,7 +21,7 @@ package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
@@ -30,17 +30,11 @@ import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifi
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.AudioSegmentRecorder;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import jmbe.iface.IAudioCodec;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -57,15 +51,15 @@ public class MBECallSequenceConverter
* @param output path to write the WAVE recording.
* @throws IOException if there is an error.
*/
public static void convert(Path input, Path output) throws IOException
public static void convert(UserPreferences userPreferences, 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);
convert(userPreferences, sequence, output);
}
public static void convert(MBECallSequence callSequence, Path outputPath)
public static void convert(UserPreferences userPreferences, MBECallSequence callSequence, Path outputPath)
{
if(callSequence == null || callSequence.isEncrypted())
{
@@ -76,7 +70,7 @@ public class MBECallSequenceConverter
{
if(callSequence.getProtocol().equals(P25P1CallSequenceRecorder.PROTOCOL))
{
P25P1AudioModule audioModule = new P25P1AudioModule(new UserPreferences(), new AliasList("mbe generator"));
P25P1AudioModule audioModule = new P25P1AudioModule(userPreferences, new AliasList("mbe generator"));
audioModule.setRecordAudio(true);
audioModule.start();
@@ -125,7 +119,7 @@ public class MBECallSequenceConverter
try
{
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath, audioSegment.getIdentifierCollection());
}
catch(IOException ioe)
{
@@ -136,45 +130,4 @@ public class MBECallSequenceConverter
}
}
}
public static void main(String[] args)
{
boolean all = true;
String path = "/home/denny/SDRTrunk/recordings";
Path input = Paths.get(path);
if(all)
{
Collection<File> mbeFiles = FileUtils.listFiles(input.toFile(), new SuffixFileFilter(".mbe"), DirectoryFileFilter.DIRECTORY);
for(File inputFile: mbeFiles)
{
Path output = Paths.get(inputFile.getAbsolutePath().replace(".mbe", ".wav"));
mLog.info("Converting: " + inputFile);
try
{
MBECallSequenceConverter.convert(inputFile.toPath(), output);
}
catch(IOException ioe)
{
mLog.error("Error", ioe);
}
}
}
else
{
Path output = Paths.get(path.replace(".mbe", ".wav"));
mLog.info("Converting: " + path);
try
{
MBECallSequenceConverter.convert(input, output);
}
catch(IOException ioe)
{
mLog.error("Error", ioe);
}
}
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.audio.playback;
@@ -25,12 +24,12 @@ import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
@@ -40,13 +39,6 @@ import io.github.dsheirer.settings.ColorSetting;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
@@ -54,8 +46,15 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener, IPreferenceUpdateListener
{
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger(AudioChannelPanel.class);
@@ -66,53 +65,54 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
public static final String PROPERTY_COLOR_MUTED = PROPERTY_PREFIX + "muted";
public static final String PROPERTY_COLOR_VALUE = PROPERTY_PREFIX + "value";
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
private AliasModel mAliasModel;
private IconModel mIconModel;
private SettingsManager mSettingsManager;
private UserPreferences mUserPreferences;
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
private Color mBackgroundColor;
private Color mLabelColor;
private Color mMutedColor;
private Color mValueColor;
private IconModel mIconModel;
private SettingsManager mSettingsManager;
private UserPreferences mUserPreferences;
private TalkgroupFormatPreference mTalkgroupFormatPreference;
private AudioOutput mAudioOutput;
private JLabel mMutedLabel = new JLabel("M");
private JLabel mChannelName = new JLabel(" ");
private JLabel mIconLabel = new JLabel(" ");
private JLabel mIdentifierLabel = new JLabel("-----");
private Identifier mIdentifier;
private List<Alias> mAliases = Collections.EMPTY_LIST;
private AliasModel mAliasModel;
private Lock mLock = new ReentrantLock();
public AudioChannelPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
AudioOutput audioOutput, AliasModel aliasModel)
public AudioChannelPanel(UserPreferences userPreferences, SettingsManager settingsManager, AliasModel aliasModel,
IconModel iconModel, AudioOutput audioOutput)
{
mIconModel = iconModel;
mSettingsManager = settingsManager;
mSettingsManager.addListener(this);
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
mSettingsManager = settingsManager;
mAliasModel = aliasModel;
mAudioOutput = audioOutput;
mIconModel = iconModel;
mSettingsManager.addListener(this);
if(mAudioOutput != null)
{
mAudioOutput.addAudioEventListener(this);
mAudioOutput.setIdentifierCollectionListener(new AudioMetadataProcessor());
}
mUserPreferences.addUpdateListener(this);
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
mBackgroundColor = SystemProperties.getInstance().get(PROPERTY_COLOR_BACKGROUND, Color.BLACK);
mLabelColor = SystemProperties.getInstance().get(PROPERTY_COLOR_LABEL, Color.LIGHT_GRAY);
mMutedColor = SystemProperties.getInstance().get(PROPERTY_COLOR_MUTED, Color.RED);
mValueColor = SystemProperties.getInstance().get(PROPERTY_COLOR_VALUE, Color.GREEN);
init();
}
public void preDestroy()
{
mUserPreferences.removeUpdateListener(this);
}
/**
* Receives preference update notifications via the event bus
* @param preferenceType that was updated
@@ -128,9 +128,6 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
public void dispose()
{
//Deregister from receiving preference update notifications
MyEventBus.getGlobalEventBus().unregister(this);
if(mAudioOutput != null)
{
mAudioOutput.removeAudioEventListener(this);
@@ -140,9 +137,6 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
private void init()
{
//Register to receive preference updates
MyEventBus.getGlobalEventBus().register(this);
setLayout(new MigLayout("align center center, insets 0 0 0 0",
"[][][align right]0[grow,fill]", ""));
setBackground(mBackgroundColor);
@@ -183,14 +177,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
break;
case AUDIO_MUTED:
case AUDIO_UNMUTED:
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
mMutedLabel.setVisible(mAudioOutput.isMuted());
}
});
EventQueue.invokeLater(() -> mMutedLabel.setVisible(mAudioOutput.isMuted()));
break;
default:
break;

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.audio.playback;
@@ -24,20 +23,20 @@ import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.settings.SettingsManager;
import java.awt.Color;
import java.awt.Component;
import java.util.List;
import net.miginfocom.swing.MigLayout;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import java.awt.Color;
import java.awt.Component;
import java.util.List;
public class AudioChannelsPanel extends JPanel
{
private static final long serialVersionUID = 1L;
public AudioChannelsPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
IAudioController controller, AliasModel aliasModel)
public AudioChannelsPanel(IAudioController controller, UserPreferences userPreferences, SettingsManager settingsManager,
AliasModel aliasModel, IconModel iconModel)
{
setLayout(new MigLayout("insets 0 0 0 0",
"[][sizegroup abc,grow,fill][][sizegroup abc,grow,fill]", "[grow,fill]"));
@@ -50,7 +49,7 @@ public class AudioChannelsPanel extends JPanel
for(int x = 0; x < outputs.size(); x++)
{
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, outputs.get(x), aliasModel));
add(new AudioChannelPanel(userPreferences, settingsManager, aliasModel, iconModel, outputs.get(x)));
if(x < outputs.size() - 1)
{
@@ -63,7 +62,7 @@ public class AudioChannelsPanel extends JPanel
if(outputs.size() == 1)
{
addSeparator();
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, null, aliasModel), "growx");
add(new AudioChannelPanel(userPreferences, settingsManager, aliasModel, iconModel, null), "growx");
}
}

View File

@@ -18,13 +18,12 @@
*/
package io.github.dsheirer.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Broadcaster;
@@ -55,7 +54,7 @@ import javax.sound.sampled.SourceDataLine;
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
* and broadcasts audio segment metadata to registered listeners (ie gui components).
*/
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>, IPreferenceUpdateListener
{
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
private int mBufferStartThreshold;
@@ -82,6 +81,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private boolean mDropDuplicates;
private long mOutputLastTimestamp = 0;
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
private AudioFormat mAudioFormat;
private Line.Info mLineInfo;
private int mRequestedBufferSize;
/**
* Single audio channel playback with automatic starting and stopping of the
@@ -102,18 +104,31 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
mMixer = mixer;
mMixerChannel = mixerChannel;
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mixerChannel.name()));
mAudioFormat = audioFormat;
mLineInfo = lineInfo;
mRequestedBufferSize = requestedBufferSize;
mUserPreferences = userPreferences;
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
start();
}
/**
* Starts and configures this audio output
*/
private void start()
{
mUserPreferences.addUpdateListener(this);
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mMixerChannel.name()));
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
try
{
mOutput = (SourceDataLine) mMixer.getLine(lineInfo);
mOutput = (SourceDataLine) mMixer.getLine(mLineInfo);
if(mOutput != null)
{
mOutput.open(audioFormat, requestedBufferSize);
mOutput.open(mAudioFormat, mRequestedBufferSize);
//Start threshold: buffer is full with 10% or less of capacity remaining
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
@@ -133,7 +148,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
}
try
@@ -144,12 +159,12 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
}
//Run the queue processor task every 100 milliseconds or 10 times a second
//Run the queue processor task every 100 milliseconds or 10 times a second
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
0, 100, TimeUnit.MILLISECONDS);
}
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
@@ -160,13 +175,10 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
catch(LineUnavailableException e)
{
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
mMixer.getMixerInfo().getName() + "]");
mMixer.getMixerInfo().getName() + "]");
}
updateToneInsertionAudioClips();
//Register to receive preference update notifications so we can update the preference items
MyEventBus.getGlobalEventBus().register(this);
}
/**
@@ -221,7 +233,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
/**
* 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)
@@ -230,7 +241,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
{
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
}
}
@@ -304,7 +315,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
if(mCurrentAudioSegment != null)
{
mCurrentAudioSegment.decrementConsumerCount();
mCurrentAudioSegment.removeLease(getClass().toString());
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
mCurrentAudioSegment = null;
broadcast(null);
@@ -327,7 +338,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
if(isThrowaway(audioSegment))
{
audioSegment.decrementConsumerCount();
audioSegment.removeLease(getClass().toString());
audioSegment = mAudioSegmentQueue.poll();
}
else
@@ -440,7 +451,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
mCanProcessAudio = false;
if(mProcessorFuture != null)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,6 @@ package io.github.dsheirer.audio.playback;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioException;
import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
@@ -30,6 +29,15 @@ import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
@@ -49,61 +57,51 @@ import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
private static ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
private static ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
private IconModel mIconModel;
private SettingsManager mSettingsManager;
private IAudioController mController;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private JButton mMuteButton;
private AudioChannelsPanel mAudioChannelsPanel;
@Resource
private AliasModel mAliasModel;
@Resource
private AudioPlaybackManager mAudioPlaybackManager;
@Resource
private IconModel mIconModel;
@Resource
private UserPreferences mUserPreferences;
@Resource
private SettingsManager mSettingsManager;
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
IAudioController controller, AliasModel aliasModel)
public AudioPanel()
{
mIconModel = iconModel;
mSettingsManager = settingsManager;
mController = controller;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mController.addControllerListener(this);
init();
}
@PostConstruct
private void init()
{
mAudioPlaybackManager.addControllerListener(this);
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
setBackground(Color.BLACK);
mMuteButton = new MuteButton();
mMuteButton.setBackground(getBackground());
add(mMuteButton);
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
mAudioChannelsPanel = new AudioChannelsPanel(mAudioPlaybackManager, mUserPreferences, mSettingsManager,
mAliasModel, mIconModel);
add(mAudioChannelsPanel);
addMouseListener(new MouseSelectionListener());
}
@PreDestroy
public void preDestroy()
{
}
@Override
public void receive(AudioEvent event)
{
@@ -112,19 +110,15 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
case AUDIO_CONFIGURATION_CHANGE_STARTED:
break;
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
remove(mAudioChannelsPanel);
mAudioChannelsPanel.dispose();
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
add(mAudioChannelsPanel);
mAudioChannelsPanel.repaint();
revalidate();
repaint();
}
EventQueue.invokeLater(() -> {
remove(mAudioChannelsPanel);
mAudioChannelsPanel.dispose();
mAudioChannelsPanel = new AudioChannelsPanel(mAudioPlaybackManager, mUserPreferences, mSettingsManager,
mAliasModel, mIconModel);
add(mAudioChannelsPanel);
mAudioChannelsPanel.repaint();
revalidate();
repaint();
});
break;
default:
@@ -147,14 +141,7 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
mAudioOutput = audioOutput;
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mAudioOutput.setMuted(!mAudioOutput.isMuted());
}
});
addActionListener(e -> mAudioOutput.setMuted(!mAudioOutput.isMuted()));
}
}
@@ -179,14 +166,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
@Override
public void actionPerformed(ActionEvent e)
{
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_PLAYBACK));
}
});
popup.add(outputMenu);
popup.add(new JPopupMenu.Separator());
/* Audio output mute and volume control */
for(AudioOutput output : mController.getAudioOutputs())
for(AudioOutput output : mAudioPlaybackManager.getAudioOutputs())
{
JMenu menu = new JMenu("Channel: " + output.getChannelName());
@@ -353,7 +340,7 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
try
{
mController.setMixerChannelConfiguration(mConfiguration);
mAudioPlaybackManager.setMixerChannelConfiguration(mConfiguration);
}
catch(AudioException e1)
{
@@ -392,7 +379,7 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
mMuted = !mMuted;
for(AudioOutput output : mController.getAudioOutputs())
for(AudioOutput output : mAudioPlaybackManager.getAudioOutputs())
{
output.setMuted(mMuted);
}

View File

@@ -18,19 +18,21 @@
*/
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.audio.call.AudioSegment;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
@@ -43,20 +45,22 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Manages scheduling and playback of audio segments to the local users audio system.
*/
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
@Component("audioPlaybackManager")
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController, IPreferenceUpdateListener
{
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ScheduledFuture<?> mProcessingTask;
@Resource
private UserPreferences mUserPreferences;
private MixerChannelConfiguration mMixerChannelConfiguration;
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
@@ -68,13 +72,15 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
/**
* Constructs an instance.
*
* @param userPreferences for audio playback preferences
*/
public AudioPlaybackManager(UserPreferences userPreferences)
public AudioPlaybackManager()
{
mUserPreferences = userPreferences;
MyEventBus.getGlobalEventBus().register(this);
}
@PostConstruct
public void postConstruct()
{
mUserPreferences.addUpdateListener(this);
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
@@ -87,8 +93,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
catch(AudioException ae)
{
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
}
}
else
@@ -97,6 +103,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
}
@PreDestroy
public void preDestroy()
{
mUserPreferences.removeUpdateListener(this);
dispose();
}
/**
* Receives audio segments from channel audio modules.
* @param audioSegment
@@ -120,9 +133,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
while(newSegment != null)
{
if(newSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
{
newSegment.decrementConsumerCount();
newSegment.removeLease(getClass().toString());
}
else if(newSegment.hasAudio())
{
@@ -148,10 +161,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
audioSegment = it.next();
if(audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
{
it.remove();
audioSegment.decrementConsumerCount();
audioSegment.removeLease(getClass().toString());
}
else if(audioSegment.hasAudio())
{
@@ -163,7 +176,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
//Rare situation: the audio segment completed but never had audio ... dispose it
it.remove();
audioSegment.decrementConsumerCount();
audioSegment.removeLease(getClass().toString());
}
}
}
@@ -182,10 +195,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
audioSegment = it.next();
if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.decrementConsumerCount();
audioSegment.removeLease(getClass().toString());
}
else if(audioSegment.isLinked())
{
@@ -194,7 +207,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
if(audioOutput.isLinkedTo(audioSegment))
{
it.remove();
audioSegment.addLease(audioOutput.getClass().toString());
audioOutput.play(audioSegment);
audioSegment.removeLease(getClass().toString());
}
}
}
@@ -211,7 +226,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
if(audioOutput.isEmpty())
{
audioOutput.play(mAudioSegments.remove(0));
AudioSegment audioSegment1 = mAudioSegments.remove(0);
audioSegment1.addLease(audioOutput.getClass().toString());
audioOutput.play(audioSegment1);
audioSegment1.removeLease(getClass().toString());
if(mAudioSegments.isEmpty())
{
@@ -228,10 +246,10 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
audioSegment = it.next();
if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.decrementConsumerCount();
audioSegment.removeLease(getClass().toString());
}
}
}
@@ -239,7 +257,6 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
if(mProcessingTask != null)
{
mProcessingTask.cancel(true);
@@ -254,7 +271,6 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
* Receive user preference update notifications so that we can detect when the user changes the audio output
* device in the user preferences editor.
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.PLAYBACK)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,12 +21,12 @@ package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import javax.sound.sampled.Mixer;
/**
* Mono Audio output implementation
*/
@@ -37,7 +37,7 @@ public class MonoAudioOutput extends AudioOutput
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
{
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO,
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,12 +21,12 @@ package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import javax.sound.sampled.Mixer;
/**
* Stereo audio output implementation.
*/

View File

@@ -0,0 +1,456 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import io.github.dsheirer.audio.call.Call;
import io.github.dsheirer.source.mixer.MixerChannel;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.media.Media;
import javafx.scene.media.MediaException;
import javafx.scene.media.MediaPlayer;
/**
* Controller for a single audio playback channel.
*/
public class AudioPlaybackChannelController implements ChangeListener<MediaPlayer.Status>
{
private static final String EMPTY = new String("EMPTY");
private static final String LOADED = new String("LOADED");
private static final String NO_FILE = new String("NO FILE");
private ObjectProperty<MediaPlayer> mMediaPlayer = new SimpleObjectProperty<>();
private StringProperty mFrequency = new SimpleStringProperty();
private StringProperty mFrom = new SimpleStringProperty();
private StringProperty mFromAlias = new SimpleStringProperty();
private StringProperty mName = new SimpleStringProperty();
private StringProperty mSite = new SimpleStringProperty();
private StringProperty mSystem = new SimpleStringProperty();
private StringProperty mTo = new SimpleStringProperty();
private StringProperty mToAlias = new SimpleStringProperty();
private ObjectProperty<PlaybackMode> mPlaybackMode = new SimpleObjectProperty<>();
private StringProperty mMediaPlayerStatus = new SimpleStringProperty(EMPTY);
private Call mCall;
private double mBalance;
private List<IAudioPlaybackStatusListener> mAudioPlaybackStatusListeners = new ArrayList<>();
private EndOfMediaRunnable mEndOfMediaRunnable = new EndOfMediaRunnable();
private CallUpdateListener mCallUpdateListener = new CallUpdateListener();
/**
* Constructs an instance.
* @param mixerChannel channel for this controller
* @param balance to effect audio playback balance where -1.0 is full left and 1.0 is full right.
*/
public AudioPlaybackChannelController(MixerChannel mixerChannel, double balance)
{
nameProperty().set(mixerChannel.getLabel());
playbackModeProperty().set(PlaybackMode.AUTO);
mBalance = balance;
}
/**
* Status of the media player.
*/
public StringProperty mediaPlayerStatusProperty()
{
return mMediaPlayerStatus;
}
/**
* Indicates if this controller is locked for playback of a specific identifier.
*/
public boolean isLocked()
{
return playbackModeProperty().get().equals(PlaybackMode.LOCKED);
}
/**
* Indicates if this controller is (already) locked to the specified call.
* @param call to check for lock match.
* @return true if this controller is locked for the system and identifier of the call.
*/
public boolean isLockedFor(Call call)
{
return playbackModeProperty().get().equals(PlaybackMode.LOCKED) &&
toProperty().get() != null &&
(toProperty().get().equals(call.getToId()) || toProperty().get().equals(call.getFromId())) &&
((systemProperty().get() == null && call.getSystem() == null) ||
(systemProperty().get() != null && systemProperty().get().equals(call.getSystem())));
}
/**
* Indicates if this controller is set for an autoplay mode (AUTO or LOCKED) and currently doesn't have a call assigned.
* @return true if available and has autoplay mode.
*/
public boolean isAvailableForAutoPlay()
{
return playbackModeProperty().get().isAutoPlayable() && mCall == null;
}
/**
* Registers the listener to be notified of audio playback status changes.
* @param listener to be registered.
*/
public void add(IAudioPlaybackStatusListener listener)
{
mAudioPlaybackStatusListeners.add(listener);
}
/**
* Broadcasts an audio playback status change to registered listeners.
*/
private void broadcastPlaybackStatus(MediaPlayer.Status previousStatus, MediaPlayer.Status currentStatus)
{
for(IAudioPlaybackStatusListener listener: mAudioPlaybackStatusListeners)
{
listener.playbackStatusUpdated(this, previousStatus, currentStatus);
}
}
/**
* Broadcasts an end-of-media event to registered listeners.
*/
private void broadcastEndOfMedia()
{
for(IAudioPlaybackStatusListener listener: mAudioPlaybackStatusListeners)
{
listener.endOfMedia(this);
}
//If we're in AUTO or LOCKED modes, clear the media player at the end of playback.
if(playbackModeProperty().get().isAutoPlayable())
{
clearMediaPlayerAndCall();
}
}
/**
* Sets playback mode to LOCKED for the identifier and system, or AUTO if the arguments are null.
* @param identifier optional identifier for LOCKED playback mode.
* @param system optional system name for LOCKED playback mode.
*/
public void lockAutoTo(String identifier, String system)
{
Platform.runLater(() -> {
clearMediaPlayerAndCall();
playbackModeProperty().set(identifier == null ? PlaybackMode.AUTO : PlaybackMode.LOCKED);
toProperty().set(identifier);
//Only set the system value if the identifier is non-null
systemProperty().set(identifier != null ? system : null);
});
}
/**
* Sets playback mode to AUTO and clears any currently playing audio.
*/
public void auto()
{
lockAutoTo(null, null);
}
/**
* Mutes this controller.
*/
public void mute()
{
Platform.runLater(() -> {
clearMediaPlayerAndCall();
playbackModeProperty().set(PlaybackMode.MUTE);
});
}
/**
* Plays the specified call and sets playback mode to REPLAY
* @param call to replay
*/
public void replay(final Call call)
{
Platform.runLater(() -> {
if(call != null && !call.equals(mCall))
{
clearMediaPlayerAndCall();
mCall = call;
mCall.playbackChannelProperty().set(nameProperty().get());
updateCallMetadata();
playbackModeProperty().set(PlaybackMode.REPLAY);
loadMediaPlayer();
}
else if(call == null)
{
clearMediaPlayerAndCall();
}
});
}
/**
* Plays the specified call. If this controller is already playing a previous call, stops that call and proceeds
* to play this call. Does not change the replay mode. At the end of media, closes the audio file and clears
* the call and call metadata.
* @param call to play
*/
public void play(final Call call)
{
Platform.runLater(() -> {
if(call != null && !call.equals(mCall))
{
clearMediaPlayerAndCall();
mCall = call;
mCall.playbackChannelProperty().set(nameProperty().get());
loadMediaPlayer();
updateCallMetadata();
}
else if(call == null)
{
clearMediaPlayerAndCall();
}
});
}
private void loadMediaPlayer()
{
if(mCall.getFile() != null)
{
File file = new File(mCall.getFile());
if(file.exists())
{
try
{
MediaPlayer mediaPlayer = new MediaPlayer(new Media(file.toURI().toString()));
//Create a new media player
mMediaPlayer.set(mediaPlayer);
//Register status listener
mMediaPlayer.get().statusProperty().addListener(this);
//Register runnable for end-of-media event
mMediaPlayer.get().onEndOfMediaProperty().set(mEndOfMediaRunnable);
//Set audio channel balance
mMediaPlayer.get().balanceProperty().set(mBalance);
//Set the media player to auto-start replay
mMediaPlayer.get().setAutoPlay(true);
mMediaPlayerStatus.set(LOADED);
}
catch(MediaException me)
{
mMediaPlayer.set(null);
mMediaPlayerStatus.set(NO_FILE);
}
}
//Register a listener on the call's last updated property for update notifications.
mCall.lastUpdatedProperty().addListener(mCallUpdateListener);
}
}
/**
* Stops playback and clears the media player and nullifies the call.
*/
private void clearMediaPlayerAndCall()
{
if(mCall != null)
{
mCall.lastUpdatedProperty().removeListener(mCallUpdateListener);
mCall.playbackChannelProperty().set(null);
}
if(mMediaPlayer.get() != null)
{
mMediaPlayer.get().statusProperty().removeListener(this);
mMediaPlayer.get().onEndOfMediaProperty().set(null);
mMediaPlayer.get().stop();
mMediaPlayer.set(null);
}
mCall = null;
updateCallMetadata();
mediaPlayerStatusProperty().set(EMPTY);
}
/**
* Media player property for this controller.
*/
public ObjectProperty<MediaPlayer> mediaPlayerProperty()
{
return mMediaPlayer;
}
/**
* Updates any call metadata that has changed.
*/
private void updateCallMetadata()
{
if(mCall != null)
{
//Only change TO and SYSTEM when we're not in locked mode
if(!playbackModeProperty().get().equals(PlaybackMode.LOCKED))
{
updateProperty(toProperty(), mCall.getToId());
updateProperty(systemProperty(), mCall.getSystem());
}
updateProperty(toAliasProperty(), mCall.getToAlias());
updateProperty(fromProperty(), mCall.getFromId());
updateProperty(fromAliasProperty(), mCall.getFromAlias());
updateProperty(frequencyProperty(), String.valueOf(mCall.getFrequency()));
updateProperty(siteProperty(), mCall.getSite());
}
else
{
//Only change TO and SYSTEM when we're not in locked mode
if(!playbackModeProperty().get().equals(PlaybackMode.LOCKED))
{
updateProperty(toProperty(), null);
updateProperty(systemProperty(), null);
}
updateProperty(toAliasProperty(), null);
updateProperty(fromProperty(), null);
updateProperty(fromAliasProperty(), null);
updateProperty(frequencyProperty(), null);
updateProperty(siteProperty(), null);
}
}
/**
* Updates the specified property if empty or if the value argument is different.
* @param property to update
* @param value that is updated.
*/
private void updateProperty(StringProperty property, String value)
{
if(property.get() == null || !property.get().equals(value))
{
property.set(value);
}
}
/**
* From identifier property
*/
public StringProperty fromProperty()
{
return mFrom;
}
/**
* From identifier alias property
*/
public StringProperty fromAliasProperty()
{
return mFromAlias;
}
/**
* To identifier property
*/
public StringProperty toProperty()
{
return mTo;
}
/**
* To identifier alias property
*/
public StringProperty toAliasProperty()
{
return mToAlias;
}
/**
* Channel frequency property
*/
public StringProperty frequencyProperty()
{
return mFrequency;
}
/**
* Site property
*/
public StringProperty siteProperty()
{
return mSite;
}
/**
* System property
*/
public StringProperty systemProperty()
{
return mSystem;
}
/**
* Playback controller name property
*/
public StringProperty nameProperty()
{
return mName;
}
/**
* Playback mode property
*/
public ObjectProperty<PlaybackMode> playbackModeProperty()
{
return mPlaybackMode;
}
/**
* Implements the change listener interface for MediaPlayer status changes.
*/
@Override
public void changed(ObservableValue<? extends MediaPlayer.Status> observable, MediaPlayer.Status previousStatus,
MediaPlayer.Status currentStatus)
{
broadcastPlaybackStatus(previousStatus, currentStatus);
mMediaPlayerStatus.set(currentStatus.name());
}
/**
* Runnable to register with the media player to detect when the end of the media is detected.
*/
private class EndOfMediaRunnable implements Runnable
{
@Override
public void run()
{
broadcastEndOfMedia();
}
}
/**
* Monitors the call's last updated property to effect updates to this controller about the call state.
*/
private class CallUpdateListener implements ChangeListener<Number>
{
@Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
{
updateCallMetadata();
}
}
}

View File

@@ -0,0 +1,271 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import jiconfont.icons.elusive.Elusive;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
/**
* Abstract audio playback channel view
*/
public abstract class AudioPlaybackChannelView extends VBox
{
protected static final String CSS_STYLE_FONT_LARGE = "-fx-font-size:16;";
protected static final String CSS_STYLE_FONT_LARGE_BOLD = CSS_STYLE_FONT_LARGE + "-fx-font-weight: bold;";
protected static final String TOOLTIP_MUTED = "Audio channel muted. Click to unmute audio channel";
protected static final String TOOLTIP_UNMUTED = "Audio channel not muted. Click to mute audio channel";
protected static final String TOOLTIP_LOCKED = "Audio channel playback locked to talkgroup. Click to unlock audio channel";
protected static final String TOOLTIP_UNLOCKED = "Audio channel playback unlocked. Click to lock audio channel to current talkgroup";
protected static final Color ICON_COLOR = Color.BLACK;
protected static final int ICON_SIZE = 20;
private AudioPlaybackChannelController mController;
private Label mFromLabel;
private Label mFromAliasLabel;
private Label mToLabel;
private Label mToAliasLabel;
private Label mPlaybackMode;
private Label mMediaPlayerStatus;
private Label mMuteLabel;
private Label mLockLabel;
private Label mSystemLabel;
/**
* Constructs an instance.
* @param controller to use for this view.
*/
public AudioPlaybackChannelView(AudioPlaybackChannelController controller)
{
mController = controller;
getController().playbackModeProperty().addListener((observable, oldValue, newValue) -> updateLockMuteState());
getToLabel().textProperty().addListener((observable, oldValue, newValue) -> updateLockMuteState());
}
/**
* Perform any actions related to resizing the width of the panel.
*/
public abstract void resetWidth();
/**
* Controller for this view.
* @return controller
*/
protected AudioPlaybackChannelController getController()
{
return mController;
}
/**
* Updates the state of the lock and mute labels (ie buttons).
*/
protected void updateLockMuteState()
{
switch(getController().playbackModeProperty().get())
{
case MUTE:
setMuted(true);
setLocked(false, true);
break;
case AUTO:
case REPLAY:
setMuted(false);
setLocked(false, getToLabel().textProperty().get() == null);
break;
case LOCKED:
setMuted(false);
setLocked(true, false);
break;
}
}
/**
* Sets the state of the lock label.
* @param locked true to show locked state, false otherwise
* @param disabled true to disable the label, false otherwise
*/
private void setLocked(boolean locked, boolean disabled)
{
IconNode iconNode = new IconNode(locked ? Elusive.LOCK : Elusive.UNLOCK);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(disabled ? Color.DARKGRAY : ICON_COLOR);
getLockLabel().setGraphic(iconNode);
getLockLabel().setTooltip(new Tooltip(locked ? TOOLTIP_LOCKED : TOOLTIP_UNLOCKED));
getLockLabel().setDisable(disabled);
}
/**
* Sets the state of the mute label.
* @param locked true to show muted state, false otherwise
*/
private void setMuted(boolean muted)
{
IconNode iconNode = new IconNode(muted ? Elusive.VOLUME_OFF : Elusive.VOLUME_UP);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(muted ? Color.RED : Color.BLACK);
getMuteLabel().setGraphic(iconNode);
getMuteLabel().setTooltip(new Tooltip(muted ? TOOLTIP_MUTED : TOOLTIP_UNMUTED));
}
protected Label getFromLabel()
{
if(mFromLabel == null)
{
mFromLabel = new Label();
mFromLabel.setStyle(CSS_STYLE_FONT_LARGE_BOLD);
mFromLabel.textProperty().bind(getController().fromProperty());
}
return mFromLabel;
}
protected Label getFromAliasLabel()
{
if(mFromAliasLabel == null)
{
mFromAliasLabel = new Label();
mFromAliasLabel.setStyle(CSS_STYLE_FONT_LARGE);
mFromAliasLabel.textProperty().bind(getController().fromAliasProperty());
}
return mFromAliasLabel;
}
protected Label getSystemLabel()
{
if(mSystemLabel == null)
{
mSystemLabel = new Label();
mSystemLabel.textProperty().bind(getController().systemProperty());
}
return mSystemLabel;
}
protected Label getToLabel()
{
if(mToLabel == null)
{
mToLabel = new Label();
mToLabel.setStyle(CSS_STYLE_FONT_LARGE_BOLD);
mToLabel.textProperty().bind(getController().toProperty());
}
return mToLabel;
}
protected Label getToAliasLabel()
{
if(mToAliasLabel == null)
{
mToAliasLabel = new Label();
mToAliasLabel.setStyle(CSS_STYLE_FONT_LARGE);
mToAliasLabel.textProperty().bind(getController().toAliasProperty());
}
return mToAliasLabel;
}
protected Label getMuteLabel()
{
if(mMuteLabel == null)
{
mMuteLabel = new Label();
mMuteLabel.setTooltip(new Tooltip(TOOLTIP_UNMUTED));
mMuteLabel.setPadding(new Insets(1));
IconNode iconNode = new IconNode(Elusive.VOLUME_UP);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(ICON_COLOR);
mMuteLabel.setGraphic(iconNode);
mMuteLabel.setOnMouseClicked(event -> {
if(getController().playbackModeProperty().get().equals(PlaybackMode.MUTE))
{
getController().auto();
}
else
{
getController().mute();
}
});
}
return mMuteLabel;
}
protected Label getLockLabel()
{
if(mLockLabel == null)
{
mLockLabel = new Label();
mLockLabel.setPadding(new Insets(1));
IconNode iconNode = new IconNode(FontAwesome.UNLOCK);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.DARKGRAY);
mLockLabel.setGraphic(iconNode);
mLockLabel.setOnMouseClicked(event -> {
if(getController().playbackModeProperty().get().equals(PlaybackMode.LOCKED))
{
getController().auto();
}
else
{
String to = getToLabel().textProperty().get();
String system = getSystemLabel().textProperty().get();
getController().lockAutoTo(to, system);
}
});
}
return mLockLabel;
}
protected Label getPlaybackMode()
{
if(mPlaybackMode == null)
{
mPlaybackMode = new Label();
mPlaybackMode.textProperty().bind(getController().playbackModeProperty().map(playbackMode -> switch(playbackMode)
{
case AUTO -> "AUTO";
case LOCKED -> "AUTO-LOCKED";
case REPLAY -> "REPLAY";
case MUTE -> "MUTED";
}));
}
return mPlaybackMode;
}
protected Label getMediaPlayerStatus()
{
if(mMediaPlayerStatus == null)
{
mMediaPlayerStatus = new Label();
mMediaPlayerStatus.setDisable(true);
mMediaPlayerStatus.textProperty().bind(getController().mediaPlayerStatusProperty());
}
return mMediaPlayerStatus;
}
}

View File

@@ -0,0 +1,213 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
/**
* Complete audio playback view
*/
public class AudioPlaybackChannelViewComplete extends AudioPlaybackChannelView
{
private AudioSpectrumView mAudioSpectrumView;
private Label mFrequencyLabel;
private Label mNameLabel;
private Label mSiteLabel;
/**
* Constructs an instance
* @param controller for audio playback that backs this view.
*/
public AudioPlaybackChannelViewComplete(AudioPlaybackChannelController controller)
{
super(controller);
//Bind the audio spectrum view to the controller's media player so that it can update as the playback changes
getAudioSpectrumView().mediaPlayerProperty().bind(getController().mediaPlayerProperty());
init();
}
/**
* Resets the spectrum width during resizing.
*/
public void resetWidth()
{
getAudioSpectrumView().resetWidth();
}
private void init()
{
setSpacing(3);
setMaxWidth(Double.MAX_VALUE);
setBorder(new Border(new BorderStroke(Color.BLACK, BorderStrokeStyle.SOLID, null, null)));
setPadding(new Insets(0));
GridPane gridPane = new GridPane();
gridPane.setPadding(new Insets(0, 0, 2, 2));
gridPane.setHgap(3);
int row = 0;
GridPane.setHalignment(getNameLabel(), HPos.RIGHT);
GridPane.setHgrow(getNameLabel(), Priority.NEVER);
gridPane.add(getNameLabel(), 0, row, 2, 1);
HBox playbackStatusIdentifierBox = new HBox();
playbackStatusIdentifierBox.setAlignment(Pos.CENTER);
HBox.setHgrow(getMediaPlayerStatus(), Priority.ALWAYS);
getMediaPlayerStatus().setMaxWidth(Double.MAX_VALUE);
getMediaPlayerStatus().setAlignment(Pos.BASELINE_RIGHT);
playbackStatusIdentifierBox.getChildren().addAll(getPlaybackMode(), getMediaPlayerStatus());
GridPane.setHalignment(playbackStatusIdentifierBox, HPos.LEFT);
GridPane.setHgrow(playbackStatusIdentifierBox, Priority.ALWAYS);
gridPane.add(playbackStatusIdentifierBox, 2, row, 2, 1);
GridPane.setHalignment(getAudioSpectrumView(), HPos.RIGHT);
GridPane.setHgrow(getAudioSpectrumView(), Priority.ALWAYS);
gridPane.add(getAudioSpectrumView(), 4, row, 1, 3);
row++;
GridPane.setHgrow(getLockLabel(), Priority.NEVER);
gridPane.add(getLockLabel(), 0, row);
Label toLabel = new Label("TO:");
toLabel.setDisable(true);
GridPane.setHalignment(toLabel, HPos.RIGHT);
GridPane.setHgrow(toLabel, Priority.NEVER);
gridPane.add(toLabel, 1, row);
GridPane.setHalignment(getToLabel(), HPos.RIGHT);
GridPane.setHgrow(getToLabel(), Priority.NEVER);
gridPane.add(getToLabel(), 2, row);
GridPane.setHalignment(getToAliasLabel(), HPos.LEFT);
GridPane.setHgrow(getToAliasLabel(), Priority.ALWAYS);
gridPane.add(getToAliasLabel(), 3, row);
row++;
GridPane.setHgrow(getMuteLabel(), Priority.NEVER);
gridPane.add(getMuteLabel(), 0, row);
Label fromLabel = new Label("FM:");
fromLabel.setDisable(true);
GridPane.setHalignment(fromLabel, HPos.RIGHT);
GridPane.setHgrow(fromLabel, Priority.NEVER);
gridPane.add(fromLabel, 1, row);
GridPane.setHalignment(getFromLabel(), HPos.RIGHT);
GridPane.setHgrow(getFromLabel(), Priority.NEVER);
gridPane.add(getFromLabel(), 2, row);
GridPane.setHalignment(getFromAliasLabel(), HPos.LEFT);
GridPane.setHgrow(getFromAliasLabel(), Priority.ALWAYS);
gridPane.add(getFromAliasLabel(), 3, row);
row++;
Label channelLabel = new Label("CHAN:");
channelLabel.setDisable(true);
GridPane.setHalignment(channelLabel, HPos.RIGHT);
GridPane.setHgrow(channelLabel, Priority.NEVER);
gridPane.add(channelLabel, 0, row, 2, 1);
GridPane.setHalignment(getFrequencyLabel(), HPos.LEFT);
GridPane.setHgrow(getFrequencyLabel(), Priority.ALWAYS);
gridPane.add(getFrequencyLabel(), 2, row);
HBox systemSiteBox = new HBox();
systemSiteBox.setSpacing(3);
Label dashLabel = new Label("-");
dashLabel.textProperty().bind(getSiteLabel().textProperty().map(s -> getSiteLabel()
.textProperty().get() != null ? " - " : null));
systemSiteBox.getChildren().addAll(getSystemLabel(), dashLabel, getSiteLabel());
GridPane.setHalignment(systemSiteBox, HPos.LEFT);
GridPane.setHgrow(systemSiteBox, Priority.ALWAYS);
gridPane.add(systemSiteBox, 3, row, 2, 1);
ColumnConstraints cc0 = new ColumnConstraints();
cc0.setPercentWidth(4);
ColumnConstraints cc1 = new ColumnConstraints();
cc1.setPercentWidth(4);
ColumnConstraints cc2 = new ColumnConstraints();
cc2.setPercentWidth(14);
ColumnConstraints cc3 = new ColumnConstraints();
cc3.setPercentWidth(28);
ColumnConstraints cc4 = new ColumnConstraints();
cc4.setPercentWidth(50);
gridPane.getColumnConstraints().addAll(cc0, cc1, cc2, cc3, cc4);
getChildren().add(gridPane);
}
private AudioSpectrumView getAudioSpectrumView()
{
if(mAudioSpectrumView == null)
{
mAudioSpectrumView = new AudioSpectrumView();
}
return mAudioSpectrumView;
}
private Label getFrequencyLabel()
{
if(mFrequencyLabel == null)
{
mFrequencyLabel = new Label();
mFrequencyLabel.textProperty().bind(getController().frequencyProperty());
}
return mFrequencyLabel;
}
private Label getSiteLabel()
{
if(mSiteLabel == null)
{
mSiteLabel = new Label();
mSiteLabel.textProperty().bind(getController().siteProperty());
}
return mSiteLabel;
}
private Label getNameLabel()
{
if(mNameLabel == null)
{
mNameLabel = new Label();
mNameLabel.setDisable(true);
mNameLabel.textProperty().bind(getController().nameProperty().concat(":"));
}
return mNameLabel;
}
}

View File

@@ -0,0 +1,95 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
/**
* Minimal audio playback view
*/
public class AudioPlaybackChannelViewMinimal extends AudioPlaybackChannelView
{
/**
* Constructs an instance
* @param controller for audio playback that backs this view.
*/
public AudioPlaybackChannelViewMinimal(AudioPlaybackChannelController controller)
{
super(controller);
init();
}
/**
* Reset width
*/
public void resetWidth()
{
//No-op
}
private void init()
{
setSpacing(3);
setMaxWidth(Double.MAX_VALUE);
setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, null, null)));
setPadding(new Insets(0, 3, 0, 3));
GridPane gridPane = new GridPane();
gridPane.setHgap(3);
int row = 0;
GridPane.setHalignment(getMuteLabel(), HPos.LEFT);
GridPane.setHgrow(getMuteLabel(), Priority.NEVER);
gridPane.add(getMuteLabel(), 0, row);
GridPane.setHalignment(getLockLabel(), HPos.LEFT);
GridPane.setHgrow(getLockLabel(), Priority.NEVER);
gridPane.add(getLockLabel(), 1, row);
Label toLabel = new Label("TO:");
toLabel.setDisable(true);
GridPane.setHalignment(toLabel, HPos.RIGHT);
GridPane.setHgrow(toLabel, Priority.NEVER);
gridPane.add(toLabel, 2, row);
GridPane.setHalignment(getToLabel(), HPos.RIGHT);
GridPane.setHgrow(getToLabel(), Priority.NEVER);
gridPane.add(getToLabel(), 3, row);
GridPane.setHalignment(getToAliasLabel(), HPos.LEFT);
GridPane.setHgrow(getToAliasLabel(), Priority.ALWAYS);
getToAliasLabel().setMaxWidth(Double.MAX_VALUE);
gridPane.add(getToAliasLabel(), 4, row);
GridPane.setHalignment(getPlaybackMode(), HPos.RIGHT);
GridPane.setHgrow(getPlaybackMode(), Priority.SOMETIMES);
gridPane.add(getPlaybackMode(), 4, row);
getChildren().add(gridPane);
}
}

View File

@@ -0,0 +1,115 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.paint.Color;
/**
* Standard audio playback view
*/
public class AudioPlaybackChannelViewStandard extends AudioPlaybackChannelView
{
/**
* Constructs an instance
* @param controller for audio playback that backs this view.
*/
public AudioPlaybackChannelViewStandard(AudioPlaybackChannelController controller)
{
super(controller);
init();
}
/**
* Reset width
*/
public void resetWidth()
{
//No-op
}
private void init()
{
setSpacing(3);
setMaxWidth(Double.MAX_VALUE);
setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, null, null)));
setPadding(new Insets(0, 3, 0, 3));
GridPane gridPane = new GridPane();
gridPane.setHgap(3);
int row = 0;
GridPane.setHalignment(getLockLabel(), HPos.LEFT);
GridPane.setHgrow(getLockLabel(), Priority.NEVER);
gridPane.add(getLockLabel(), 0, row);
Label toLabel = new Label("TO:");
toLabel.setDisable(true);
GridPane.setHalignment(toLabel, HPos.RIGHT);
GridPane.setHgrow(toLabel, Priority.NEVER);
gridPane.add(toLabel, 1, row);
GridPane.setHalignment(getToLabel(), HPos.RIGHT);
GridPane.setHgrow(getToLabel(), Priority.NEVER);
gridPane.add(getToLabel(), 2, row);
GridPane.setHalignment(getToAliasLabel(), HPos.LEFT);
GridPane.setHgrow(getToAliasLabel(), Priority.ALWAYS);
getToAliasLabel().setMaxWidth(Double.MAX_VALUE);
gridPane.add(getToAliasLabel(), 3, row);
GridPane.setHalignment(getPlaybackMode(), HPos.RIGHT);
GridPane.setHgrow(getPlaybackMode(), Priority.SOMETIMES);
gridPane.add(getPlaybackMode(), 4, row);
row++;
GridPane.setHalignment(getMuteLabel(), HPos.LEFT);
GridPane.setHgrow(getMuteLabel(), Priority.NEVER);
gridPane.add(getMuteLabel(), 0, row);
Label fromLabel = new Label("FM:");
fromLabel.setDisable(true);
GridPane.setHalignment(fromLabel, HPos.RIGHT);
GridPane.setHgrow(fromLabel, Priority.NEVER);
gridPane.add(fromLabel, 1, row);
GridPane.setHalignment(getFromLabel(), HPos.RIGHT);
GridPane.setHgrow(getFromLabel(), Priority.NEVER);
gridPane.add(getFromLabel(), 2, row);
GridPane.setHalignment(getFromAliasLabel(), HPos.LEFT);
GridPane.setHgrow(getFromAliasLabel(), Priority.ALWAYS);
gridPane.add(getFromAliasLabel(), 3, row);
GridPane.setHalignment(getMediaPlayerStatus(), HPos.RIGHT);
GridPane.setHgrow(getMediaPlayerStatus(), Priority.SOMETIMES);
gridPane.add(getMediaPlayerStatus(), 4, row);
getChildren().add(gridPane);
}
}

View File

@@ -0,0 +1,179 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import javafx.geometry.Insets;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
/**
* Audio playback view for one or more audio playback channel views and controllers.
*/
public class AudioPlaybackChannelsView extends HBox implements IPreferenceUpdateListener
{
@Resource
private AudioPlaybackController mAudioPlaybackController;
@Resource
private UserPreferences mUserPreferences;
private List<AudioPlaybackChannelView> mAudioPlaybackChannelViews = new ArrayList<>();
private GridPane mGridPane;
private PlaybackView mPlaybackView;
private IPanelRevalidationRequestListener mListener;
/**
* Constructs an instance
*/
public AudioPlaybackChannelsView()
{
setPadding(new Insets(0, 3, 0, 2));
setSpacing(3);
setMaxWidth(Double.MAX_VALUE);
//Force the audio spectrum view's image view to (temporarily) shrink to 1 so that we can resize dynamically.
widthProperty().addListener((observable, oldValue, newValue) -> resetWidth());
}
/**
* Registers a listener to receive requests from this view for Swing revalidation
* @param listener to receive request.
*/
public void setPanelRevalidationRequestListener(IPanelRevalidationRequestListener listener)
{
mListener = listener;
}
/**
* Implements preference type update listener to be notified if the user changes the playback view preference.
* @param preferenceType that was updated
*/
@Override
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.PLAYBACK)
{
PlaybackView preferred = mUserPreferences.getPlaybackPreference().getPlaybackView();
if(mPlaybackView != preferred)
{
mPlaybackView = preferred;
updatePlaybackViews();
//Since this view is embedded in a JFXPanel, the parent has to nullify the JFXPanel's scene and reset it.
if(mListener != null)
{
mListener.revalidatePanel();
}
}
}
}
/**
* Resets the audio spectrum view width during resizing.
*/
public void resetWidth()
{
for(AudioPlaybackChannelView view: mAudioPlaybackChannelViews)
{
view.resetWidth();
}
}
private void updatePlaybackViews()
{
if(!mAudioPlaybackChannelViews.isEmpty())
{
for(AudioPlaybackChannelView view: mAudioPlaybackChannelViews)
{
getGridPane().getChildren().remove(view);
}
mAudioPlaybackChannelViews.clear();
}
AudioPlaybackChannelController left = mAudioPlaybackController.getChannelController(MixerChannel.LEFT);
AudioPlaybackChannelView leftView = switch(mPlaybackView)
{
case MINIMAL -> new AudioPlaybackChannelViewMinimal(left);
case STANDARD -> new AudioPlaybackChannelViewStandard(left);
default -> new AudioPlaybackChannelViewComplete(left);
};
getGridPane().add(leftView, 0, 0);
mAudioPlaybackChannelViews.add(leftView);
AudioPlaybackChannelController right = mAudioPlaybackController.getChannelController(MixerChannel.RIGHT);
AudioPlaybackChannelView rightView = switch(mPlaybackView)
{
case MINIMAL -> new AudioPlaybackChannelViewMinimal(right);
case STANDARD -> new AudioPlaybackChannelViewStandard(right);
default -> new AudioPlaybackChannelViewComplete(right);
};
getGridPane().add(rightView, 1, 0);
mAudioPlaybackChannelViews.add(rightView);
}
/**
* Post instantiation/startup steps.
*/
@PostConstruct
public void postConstruct()
{
mPlaybackView = mUserPreferences.getPlaybackPreference().getPlaybackView();
//Register for notifications of user changes to the audio playback view style
mUserPreferences.addUpdateListener(this);
HBox.setHgrow(getGridPane(), Priority.ALWAYS);
updatePlaybackViews();
getChildren().add(getGridPane());
}
private GridPane getGridPane()
{
if(mGridPane == null)
{
mGridPane = new GridPane();
mGridPane.setMaxWidth(Double.MAX_VALUE);
mGridPane.setHgap(3);
//Add two columns and give them each 50% of the width
ColumnConstraints ccLeft = new ColumnConstraints();
ccLeft.setPercentWidth(100.0 / 2);
ccLeft.setHgrow(Priority.ALWAYS);
mGridPane.getColumnConstraints().add(ccLeft);
ColumnConstraints ccRight = new ColumnConstraints();
ccRight.setPercentWidth(100.0 / 2);
ccRight.setHgrow(Priority.ALWAYS);
mGridPane.getColumnConstraints().add(ccRight);
}
return mGridPane;
}
}

View File

@@ -0,0 +1,357 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.audio.call.Call;
import io.github.dsheirer.audio.call.ICallEventListener;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
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;
import javafx.scene.media.MediaPlayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Controller for application audio playback channels
*/
@Component("audioPlaybackController")
public class AudioPlaybackController implements ICallEventListener, IAudioPlaybackStatusListener
{
private static final Logger LOG = LoggerFactory.getLogger(AudioPlaybackController.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LOG);
private static final double AUDIO_BALANCE_LEFT = -1.0;
private static final double AUDIO_BALANCE_RIGHT = 1.0;
private Map<MixerChannel, AudioPlaybackChannelController> mControllerMap = new HashMap<>();
private LinkedTransferQueue<Call> mNewCallQueue = new LinkedTransferQueue<>();
private List<Call> mCalls = new ArrayList<>();
private LinkedTransferQueue<PlaybackRequest> mRequestQueue = new LinkedTransferQueue<>();
private ScheduledFuture<?> mQueueProcessorFuture;
private int mMaxCallQueueSize = 25;
/**
* Constructs an instance
*/
public AudioPlaybackController()
{
mControllerMap.put(MixerChannel.LEFT, new AudioPlaybackChannelController(MixerChannel.LEFT, AUDIO_BALANCE_LEFT));
mControllerMap.put(MixerChannel.RIGHT, new AudioPlaybackChannelController(MixerChannel.RIGHT, AUDIO_BALANCE_RIGHT));
//Register as audio playback status listener
for(AudioPlaybackChannelController controller: mControllerMap.values())
{
controller.add(this);
}
}
/**
* Post instantiation/startup steps.
*/
@PostConstruct
public void postConstruct()
{
if(mQueueProcessorFuture == null)
{
//Run at 250ms or four times a second
mQueueProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new QueueProcessor(),
0, 250, TimeUnit.MILLISECONDS);
}
}
/**
* Pre-destruction or shutdown steps
*/
@PreDestroy
public void preDestroy()
{
if(mQueueProcessorFuture != null)
{
mQueueProcessorFuture.cancel(true);
}
mRequestQueue.clear();
mNewCallQueue.clear();
mCalls.clear();
}
/**
* Process request and call queues and manage playback state of both channels. This method is intended to be
* invoked by a (single) threaded processor.
*/
private void process()
{
//Add new calls to the calls list.
mNewCallQueue.drainTo(mCalls);
//Process the request queue
PlaybackRequest request = mRequestQueue.poll();
while(request != null)
{
AudioPlaybackChannelController controller = mControllerMap.get(request.mixerChannel);
switch(request.playbackMode)
{
case AUTO -> controller.auto();
case LOCKED -> controller.lockAutoTo(request.identifier, request.system);
case MUTE -> controller.mute();
case REPLAY ->
{
controller.replay(request.call);
mCalls.remove(request.call);
}
}
request = mRequestQueue.poll();
}
//Remove duplicate calls
mCalls.removeIf(call -> call.isDuplicate());
if(mCalls.isEmpty())
{
return;
}
AudioPlaybackChannelController left = mControllerMap.get(MixerChannel.LEFT);
AudioPlaybackChannelController right = mControllerMap.get(MixerChannel.LEFT);
//Reduce calls queue size to max size but don't remove any calls tagged for locked replay.
int excessCalls = mCalls.size() - mMaxCallQueueSize;
if(excessCalls > 0)
{
Iterator<Call> it = mCalls.iterator();
while(it.hasNext() && excessCalls > 0)
{
Call call = it.next();
if(!(left.isLockedFor(call) || right.isLockedFor(call)))
{
it.remove();
excessCalls--;
System.out.println("Removed excess call TO:" + call.getToId() + " FM:" + call.getFromId());
}
}
}
//Process left audio channel if it is currently empty and set for auto replay.
if(left.isAvailableForAutoPlay())
{
List<Call> leftCalls = new ArrayList<>(mCalls);
//Remove any calls locked to the right channel
if(right.isLocked())
{
Iterator<Call> it = leftCalls.iterator();
while(it.hasNext())
{
if(right.isLockedFor(it.next()))
{
it.remove();;
}
}
}
if(left.isLocked())
{
for(int x = 0; x < leftCalls.size(); x++)
{
Call call = leftCalls.get(x);
if(left.isLockedFor(call))
{
mCalls.remove(call);
left.play(call);
break;
}
}
}
else
{
Call call = leftCalls.get(0);
mCalls.remove(call);
System.out.println("Playing call to: " + call.getToId() + " fm:" + call.getFromId());
left.play(call);
}
}
if(mCalls.isEmpty())
{
return;
}
//Process right audio channel if it is currently empty and set for auto replay.
if(right.isAvailableForAutoPlay())
{
List<Call> rightCalls = new ArrayList<>(mCalls);
//Remove any calls locked to the left channel
if(left.isLocked())
{
Iterator<Call> it = rightCalls.iterator();
while(it.hasNext())
{
if(left.isLockedFor(it.next()))
{
it.remove();;
}
}
}
if(right.isLocked())
{
for(int x = 0; x < rightCalls.size(); x++)
{
Call call = rightCalls.get(x);
if(right.isLockedFor(call))
{
mCalls.remove(call);
right.play(call);
break;
}
}
}
else
{
Call call = rightCalls.get(0);
mCalls.remove(call);
right.play(call);
}
}
}
/**
* Request replay of the specified call.
* @param mixerChannel for the replay
* @param call to replay.
*/
public void replay(MixerChannel mixerChannel, Call call)
{
mRequestQueue.add(new PlaybackRequest(mixerChannel, PlaybackMode.REPLAY, call, null, null));
}
/**
* Request auto-playback mode and optionally specifies a locked identifier.
* @param mixerChannel for the auto mode.
* @param identifier (optional) for locked replay.
* @param system (optional) for locked replay.
*/
public void auto(MixerChannel mixerChannel, String identifier, String system)
{
mRequestQueue.add(new PlaybackRequest(mixerChannel,
(identifier == null ? PlaybackMode.AUTO : PlaybackMode.LOCKED), null, identifier, system));
}
/**
* Mutes the specified mixer channel.
* @param mixerChannel to mute.
*/
public void mute(MixerChannel mixerChannel)
{
mRequestQueue.add(new PlaybackRequest(mixerChannel, PlaybackMode.MUTE, null, null, null));
}
@Override
public void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus,
MediaPlayer.Status currentStatus)
{
//TODO: do I need this?
}
@Override
public void endOfMedia(AudioPlaybackChannelController controller)
{
//TODO: do I need this?
}
/**
* Primary audio controller for manual playback.
*/
public AudioPlaybackChannelController getPrimaryController()
{
return mControllerMap.get(MixerChannel.LEFT);
}
/**
* Get the channel controller associated with the mixer channel.
* @return controller
*/
public AudioPlaybackChannelController getChannelController(MixerChannel mixerChannel)
{
return mControllerMap.get(mixerChannel);
}
@Override
public void added(Call call)
{
mNewCallQueue.add(call);
}
@Override
public void updated(Call call) {}
@Override
public void completed(Call call, AudioSegment audioSegment) {}
@Override
public void deleted(Call call) {}
/**
* Playback request
* @param mixerChannel for the request (LEFT or RIGHT)
* @param playbackMode to apply
* @param call (optional) to replay
* @param identifier (optional) for locked replay.
* @param system (optional) for locked replay.
*/
private record PlaybackRequest(MixerChannel mixerChannel, PlaybackMode playbackMode, Call call,
String identifier, String system)
{
}
/**
* Runnable queue processor.
*/
private class QueueProcessor implements Runnable
{
@Override
public void run()
{
try
{
process();
}
catch(Throwable t)
{
LOGGING_SUPPRESSOR.error(t.getMessage(), 3, "Error while processing audio queue", t);
}
}
}
}

View File

@@ -0,0 +1,260 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import java.util.Arrays;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Background;
import javafx.scene.layout.VBox;
import javafx.scene.media.AudioSpectrumListener;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
import javafx.util.Duration;
/**
* Audio spectrum view provides visual display of the frequency content of the replay audio.
*/
public class AudioSpectrumView extends VBox implements AudioSpectrumListener
{
private ObjectProperty<MediaPlayer> mMediaPlayer = new SimpleObjectProperty<>();
private static final int AUDIO_SPECTRUM_FFT_BANDS = 64;
private static final int HEIGHT = 64; //64-FFT bins plotted with one pixel per bin
private Color mColor = Color.YELLOW;
private PixelFormat mPixelFormat;
private WritableImage mWritableImage;
private ImageView mImageView;
private int mColorIntensity = 10;
private int mTimeAxisOffset = 0;
private int mPreviousPlaybackLine;
private byte[] mPlaybackLine;
private byte[] mClearedPlaybackLine;
/**
* Constructs an instance
*/
public AudioSpectrumView()
{
setHeight(64);
setBackground(Background.fill(Color.BLACK));
updateColor();
getChildren().add(getImageView());
//Monitor the width property to dynamically resize the writable image
widthProperty().addListener((observable, oldValue, newWidth) -> {
mWritableImage = new WritableImage(newWidth.intValue(), HEIGHT);
mImageView.setImage(mWritableImage);
});
//Allow user to click within the spectrum area to seek playback to the click location
setOnMouseClicked(event -> {
double requestedPlayback = (mTimeAxisOffset + event.getX()) / 20.0;
MediaPlayer mediaPlayer = mediaPlayerProperty().get();
if(mediaPlayer != null)
{
mediaPlayer.seek(Duration.seconds(requestedPlayback));
}
});
//Monitor the media player to register and deregister from audio spectrum results and DFT bin sizing.
mediaPlayerProperty().addListener((observable, oldValue, newValue) -> {
if(oldValue != null)
{
oldValue.setAudioSpectrumListener(null);
reset();
}
if(newValue != null)
{
//Register to receive audio spectrum FFT bins
newValue.setAudioSpectrumListener(AudioSpectrumView.this::spectrumDataUpdate);
//Change the FFT bin setting from default 128 bins
newValue.setAudioSpectrumNumBands(AUDIO_SPECTRUM_FFT_BANDS);
}
});
}
/**
* Resets the width when the parent container is resizing. Sets the width to 1 so that when new FFT bins come
* in, the image can be updated to the current width.
*/
public void resetWidth()
{
mWritableImage = new WritableImage(1, HEIGHT);
mImageView.setImage(mWritableImage);
}
/**
* Media player property. Bind this property to a media player so that it can register to receive audio spectrum
* data and control the FFT bins setting.
*/
public ObjectProperty<MediaPlayer> mediaPlayerProperty()
{
return mMediaPlayer;
}
/**
* Resets the spectral view
*/
public void reset()
{
mWritableImage = null;
getImageView().setImage(getWritableImage());
}
/**
* Image view for displaying the writable image.
*/
private ImageView getImageView()
{
if(mImageView == null)
{
mImageView = new ImageView(getWritableImage());
}
return mImageView;
}
/**
* Writable image for plotting FFT spectrum results.
*/
private WritableImage getWritableImage()
{
if(mWritableImage == null)
{
mWritableImage = new WritableImage(1, HEIGHT); //Initial width ... gets reset once visible.
}
return mWritableImage;
}
/**
* Updates the pixel format color array when the base color changes.
*/
private void updateColor()
{
int[] argbColors = new int[256];
int red = (int)(mColor.getRed() * 255);
int green = (int)(mColor.getGreen() * 255);
int blue = (int)(mColor.getBlue() * 255);
int rgb = ((red & 0xFF) << 16) + ((green & 0xFF) << 8) + (blue & 0xFF);
for(int alpha = 0; alpha < 256; alpha++)
{
int tempAlpha = alpha * mColorIntensity;
if(tempAlpha > 255)
{
tempAlpha = 255;
}
argbColors[alpha] = ((tempAlpha & 0xFF) << 24) + rgb;
}
mPixelFormat = PixelFormat.createByteIndexedInstance(argbColors);
}
/**
* Implements the audio spectrum listener interface to receive FFT bin results during audio playback.
*
* @param timestamp timestamp of the event in seconds.
* @param duration duration for which the spectrum was computed in seconds.
* @param magnitudes array containing the non-positive spectrum magnitude in decibels
* (dB) for each band.
* The size of the array equals the number of bands and should be considered
* to be read-only.
* @param phases array containing the phase in the range
* [<code>Math.PI</code>,&nbsp;<code>Math.PI</code>] for each band.
* The size of the array equals the number of bands and should be considered
* to be read-only.
*/
@Override
public void spectrumDataUpdate(double timestamp, double duration, float[] magnitudes, float[] phases)
{
if(mWritableImage.getWidth() != getWidth())
{
mWritableImage = new WritableImage((int)getWidth(), HEIGHT);
mImageView.setImage(mWritableImage);
}
if(mPlaybackLine == null || mPlaybackLine.length != magnitudes.length)
{
mPlaybackLine = new byte[magnitudes.length];
Arrays.fill(mPlaybackLine, (byte)(0xFF & 255));
mClearedPlaybackLine = new byte[magnitudes.length]; //Default to all zeros
}
int timeAxis = (int)Math.floor(timestamp * 10) * 2;
//Detect when time axis offset is non-zero and playback resets to zero-ish so we can reset.
if(timeAxis < mTimeAxisOffset)
{
mTimeAxisOffset = 0;
}
else
{
timeAxis -= mTimeAxisOffset;
}
//If the playback time exceeds the width of the display, shift the displayed content to the left by 25% and
//continue rendering. This will make for an animated left scrolling audio playback.
if(timeAxis > (getWritableImage().getWidth() - 3))
{
int offset = (int)(getWidth() / 4);
//Shift the current image to the left by the offset
mWritableImage.getPixelWriter().setPixels(0, 0, (int)(mWritableImage.getWidth() - offset),
HEIGHT, mWritableImage.getPixelReader(), offset, 0);
mWritableImage.getPixelWriter().setPixels((int)mWritableImage.getWidth() - offset, 0, offset, HEIGHT,
mPixelFormat, new byte[offset * HEIGHT], 0, 1);
mTimeAxisOffset += offset;
timeAxis -= offset;
}
final byte[] values = new byte[magnitudes.length];
for(int x = 0; x < magnitudes.length; x++)
{
float pixelValues = magnitudes[x];
if(pixelValues <= -60f)
{
values[magnitudes.length - x - 1] = 0;
}
else
{
values[magnitudes.length - x - 1] = (byte)((int)((60.0f + pixelValues) / 60.0f * 256.0) & 0xFF);
}
}
//Clear the previous playback line
if(mPreviousPlaybackLine < getWritableImage().getWidth())
{
getWritableImage().getPixelWriter().setPixels(mPreviousPlaybackLine, 0, 1, mClearedPlaybackLine.length,
mPixelFormat, mClearedPlaybackLine, 0, 1);
}
getWritableImage().getPixelWriter().setPixels(timeAxis++, 0, 1, values.length, mPixelFormat, values, 0, 1);
getWritableImage().getPixelWriter().setPixels(timeAxis++, 0, 1, values.length, mPixelFormat, values, 0, 1);
getWritableImage().getPixelWriter().setPixels(timeAxis, 0, 1, mPlaybackLine.length, mPixelFormat, mPlaybackLine, 0, 1);
mPreviousPlaybackLine = timeAxis;
}
}

View File

@@ -0,0 +1,41 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import javafx.scene.media.MediaPlayer;
/**
* Listener interface for audio playback status events.
*/
public interface IAudioPlaybackStatusListener
{
/**
* Notify the listener that the status of the media player has changed.
* @param controller for the media player
* @param previousStatus of the media player.
* @param currentStatus of the media player.
*/
void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus, MediaPlayer.Status currentStatus);
/**
* Notifies the listener that current playback encountered the end of the audio media.
*/
void endOfMedia(AudioPlaybackChannelController controller);
}

View File

@@ -0,0 +1,28 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
/**
* Interface to receive request for a Swing UI revaliation request from within JavaFX.
*/
public interface IPanelRevalidationRequestListener
{
void revalidatePanel();
}

View File

@@ -0,0 +1,44 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
import java.util.EnumSet;
/**
* Playback modes for left/right audio channels.
*/
public enum PlaybackMode
{
AUTO,
REPLAY,
LOCKED,
MUTE;
private static final EnumSet<PlaybackMode> AUTO_PLAY_MODES = EnumSet.of(AUTO, LOCKED);
/**
* Indicates if the playback mode is an autoplay mode (AUTO or LOCKED).
* @return true if an autoplay mode.
*/
public boolean isAutoPlayable()
{
return AUTO_PLAY_MODES.contains(this);
}
}

View File

@@ -0,0 +1,47 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playbackfx;
/**
* Audio playback view enumeration
*/
public enum PlaybackView
{
RICH("Rich"),
STANDARD("Standard"),
MINIMAL("Minimal");
private String mLabel;
/**
* Constructs an instance
* @param label to use for display purposes
*/
PlaybackView(String label)
{
mLabel = label;
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -20,6 +20,7 @@ package io.github.dsheirer.bits;
import io.github.dsheirer.edac.CRC;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.BitSet;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
@@ -306,8 +307,8 @@ public class BinaryMessage extends BitSet
/**
* Overrides the parent method which returns a BitSet so that we can return a BinaryMessage
* @param from
* @param to
* @param from inclusive
* @param to exclusive
* @return
*/
@Override
@@ -1197,9 +1198,22 @@ public class BinaryMessage extends BitSet
{
byte[] bytes = new byte[characterCount];
int index;
for(int x = 0; x < characterCount; x++)
{
bytes[x] = getByte(CHARACTER_7_BIT, x * 7 + offset);
index = x * 7 + offset;
if(index == 0)
{
bytes[x] = getByte(CHARACTER_8_BIT, index);
bytes[x] = (byte)((bytes[x] >> 1) & 0x7F);
}
else
{
index--;
bytes[x] = getByte(CHARACTER_8_BIT, index);
bytes[x] &= (byte)0x7F; //mask off the high bit since we have to parse it as an 8-bit byte.
}
}
return new String(bytes);
@@ -1278,7 +1292,7 @@ public class BinaryMessage extends BitSet
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes);
return new String(bytes, Charset.forName("UTF-16"));
}
/**

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,8 +14,8 @@
*
* 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.details;
import io.github.dsheirer.channel.state.DecoderState;
@@ -23,6 +23,11 @@ import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import net.miginfocom.swing.MigLayout;
import javax.swing.JButton;
@@ -31,29 +36,25 @@ import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.text.DefaultCaret;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ChannelDetailPanel extends JPanel implements Listener<ProcessingChain>
{
private static final String EMPTY_DETAILS = "Please select a channel to view details";
@Resource
private ChannelProcessingManager mChannelProcessingManager;
private JLabel mSystemLabel;
private JLabel mSiteLabel;
private JLabel mNameLabel;
private JTextArea mDetailTextPane;
private ChannelProcessingManager mChannelProcessingManager;
private ProcessingChain mProcessingChain;
public ChannelDetailPanel(ChannelProcessingManager channelProcessingManager)
public ChannelDetailPanel()
{
mChannelProcessingManager = channelProcessingManager;
init();
}
@PostConstruct
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0", "[grow,fill]", "[]0[grow,fill]"));

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,11 +35,10 @@ import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Channel metadata containing details about the channel configuration, decoder state and current
@@ -66,17 +65,27 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
private AliasModel mAliasModel;
private AliasList mAliasList;
/**
* Constructs an instance
* @param aliasModel for alias lookup
* @param timeslot for this metadata
*/
public ChannelMetadata(AliasModel aliasModel, Integer timeslot)
{
mAliasModel = aliasModel;
mTimeslot = timeslot;
}
/**
* Constructs an instance with timeslot (default = 0)
* @param aliasModel for alias lookup
*/
public ChannelMetadata(AliasModel aliasModel)
{
this(aliasModel, null);
mAliasModel = aliasModel;
}
public Integer getTimeslot()
{
return mTimeslot;

View File

@@ -1,46 +1,48 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.channel.metadata;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.table.AbstractTableModel;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener
import javax.swing.table.AbstractTableModel;
@Component("channelMetadataModel")
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener, IPreferenceUpdateListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataModel.class);
@@ -59,17 +61,30 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
private List<ChannelMetadata> mChannelMetadata = new ArrayList();
private Map<ChannelMetadata,Channel> mMetadataChannelMap = new HashMap();
private Listener<ChannelAndMetadata> mChannelAddListener;
@Resource
private UserPreferences mUserPreferences;
public ChannelMetadataModel()
{
MyEventBus.getGlobalEventBus().register(this);
}
@PostConstruct
public void postConstruct()
{
mUserPreferences.addUpdateListener(this);
}
@PreDestroy
public void preDestroy()
{
mUserPreferences.removeUpdateListener(this);
}
/**
* Receives preference update notifications via the event bus
* @param preferenceType that was updated
*/
@Subscribe
@Override
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.TALKGROUP_FORMAT)
@@ -84,11 +99,6 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
}
}
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
}
/**
* Lookup model row for the specified channel metadata
*/
@@ -241,7 +251,8 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
}
else
{
String value = id.getValue().replace(" TS0", "").replace(" TS1", "");
String value = id.getValue().replace(" TS1", "")
.replace(" TS2", "");
return value + " TS:" + channelMetadata.getTimeslot();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.playlist.channel.ViewChannelRequest;
@@ -32,12 +31,21 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -52,23 +60,19 @@ import javax.swing.SwingConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
public class ChannelMetadataPanel extends JPanel implements ListSelectionListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class);
private static final String TABLE_PREFERENCE_KEY = "channel.metadata.panel";
private ChannelModel mChannelModel;
@Resource
private ChannelProcessingManager mChannelProcessingManager;
@Resource
private IconModel mIconModel;
@Resource
private ChannelMetadataModel mChannelMetadataModel;
@Resource
private UserPreferences mUserPreferences;
private JTable mTable;
private Broadcaster<ProcessingChain> mSelectedProcessingChainBroadcaster = new Broadcaster<>();
@@ -80,25 +84,20 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
/**
* Table view for currently decoding channel metadata
*/
public ChannelMetadataPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences)
public ChannelMetadataPanel()
{
mChannelModel = playlistManager.getChannelModel();
mChannelProcessingManager = playlistManager.getChannelProcessingManager();
mIconModel = iconModel;
mUserPreferences = userPreferences;
init();
}
/**
* Initializes the panel
*/
private void init()
@PostConstruct
private void postConstruct()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
mTable = new JTable(mChannelProcessingManager.getChannelMetadataModel());
mChannelProcessingManager.getChannelMetadataModel().setChannelAddListener(new ChannelAddListener());
mTable = new JTable(mChannelMetadataModel);
mChannelMetadataModel.setChannelAddListener(new ChannelAddListener());
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class);
renderer.setHorizontalAlignment(SwingConstants.CENTER);
@@ -119,7 +118,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
.setCellRenderer(new FrequencyCellRenderer());
//Add a table column width monitor to store/restore column widths
mTableColumnMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
mTableColumnMonitor = new JTableColumnWidthMonitor(mTable, TABLE_PREFERENCE_KEY);
JScrollPane scrollPane = new JScrollPane(mTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
@@ -167,14 +166,11 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
{
int selectedModelRow = mTable.convertRowIndexToModel(selectedViewRow);
ChannelMetadata selectedMetadata = mChannelProcessingManager.getChannelMetadataModel()
.getChannelMetadata(selectedModelRow);
ChannelMetadata selectedMetadata = mChannelMetadataModel.getChannelMetadata(selectedModelRow);
if(selectedMetadata != null)
{
mUserSelectedChannel = mChannelProcessingManager.getChannelMetadataModel()
.getChannelFromMetadata(selectedMetadata);
mUserSelectedChannel = mChannelMetadataModel.getChannelFromMetadata(selectedMetadata);
processingChain = mChannelProcessingManager.getProcessingChain(mUserSelectedChannel);
}
}
@@ -404,12 +400,11 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
if(modelRowIndex >= 0)
{
ChannelMetadata metadata = mChannelProcessingManager.getChannelMetadataModel().getChannelMetadata(modelRowIndex);
ChannelMetadata metadata = mChannelMetadataModel.getChannelMetadata(modelRowIndex);
if(metadata != null)
{
Channel channel = mChannelProcessingManager.getChannelMetadataModel()
.getChannelFromMetadata(metadata);
Channel channel = mChannelMetadataModel.getChannelFromMetadata(metadata);
if(channel != null)
{
@@ -455,7 +450,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
if(metadata.size() > 0)
{
int modelRow = mChannelProcessingManager.getChannelMetadataModel().getRow(metadata.get(0));
int modelRow = mChannelMetadataModel.getRow(metadata.get(0));
if(modelRow >= 0)
{

View File

@@ -22,66 +22,113 @@ import com.jidesoft.swing.JideSplitPane;
import com.jidesoft.swing.JideTabbedPane;
import io.github.dsheirer.channel.details.ChannelDetailPanel;
import io.github.dsheirer.gui.power.ChannelPowerPanel;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.settings.SettingsManager;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.Color;
import net.miginfocom.swing.MigLayout;
import javax.swing.JPanel;
/**
* Swing panel for Now Playing channels table and channel details tab set.
*/
public class NowPlayingPanel extends JPanel
{
private final ChannelMetadataPanel mChannelMetadataPanel;
private final ChannelDetailPanel mChannelDetailPanel;
private final DecodeEventPanel mDecodeEventPanel;
private final MessageActivityPanel mMessageActivityPanel;
private final ChannelPowerPanel mChannelPowerPanel;
@Resource
private ChannelDetailPanel mChannelDetailPanel;
@Resource
private ChannelMetadataPanel mChannelMetadataPanel;
@Resource
private ChannelPowerPanel mChannelPowerPanel;
@Resource
private DecodeEventPanel mDecodeEventPanel;
private final MessageActivityPanel mMessageActivityPanel = new MessageActivityPanel();
private JideTabbedPane mTabbedPane;
private JideSplitPane mSplitPane;
private boolean mDetailTabsVisible = true;
/**
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
* messages, events, and spectral view.
*/
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
SettingsManager settingsManager)
public NowPlayingPanel()
{
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
init();
}
private void init()
@PostConstruct
public void postConstruct()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
getSplitPane().add(mChannelMetadataPanel);
JideTabbedPane tabbedPane = new JideTabbedPane();
tabbedPane.addTab("Details", mChannelDetailPanel);
tabbedPane.addTab("Events", mDecodeEventPanel);
tabbedPane.addTab("Messages", mMessageActivityPanel);
tabbedPane.addTab("Channel", mChannelPowerPanel);
tabbedPane.setFont(this.getFont());
tabbedPane.setForeground(Color.BLACK);
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
tabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(tabbedPane.getSelectedIndex() == tabbedPane
.indexOfComponent(mChannelPowerPanel)));
JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
splitPane.setShowGripper(true);
splitPane.add(mChannelMetadataPanel);
splitPane.add(tabbedPane);
add(splitPane);
if(mDetailTabsVisible)
{
getSplitPane().add(getTabbedPane());
}
add(getSplitPane());
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
}
/**
* Change the visibility of the channel details tabs panel.
* @param visible true to show or false to hide.
*/
public void setDetailTabsVisible(boolean visible)
{
//Only adjust if there is a change in state
if(visible ^ mDetailTabsVisible)
{
mDetailTabsVisible = visible;
if(mDetailTabsVisible)
{
getSplitPane().add(getTabbedPane());
}
else
{
getSplitPane().remove(getTabbedPane());
}
revalidate();
}
}
private JideTabbedPane getTabbedPane()
{
if(mTabbedPane == null)
{
mTabbedPane = new JideTabbedPane();
mTabbedPane.addTab("Details", mChannelDetailPanel);
mTabbedPane.addTab("Events", mDecodeEventPanel);
mTabbedPane.addTab("Messages", mMessageActivityPanel);
mTabbedPane.addTab("Channel", mChannelPowerPanel);
mTabbedPane.setFont(this.getFont());
mTabbedPane.setForeground(Color.BLACK);
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
mTabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
.indexOfComponent(mChannelPowerPanel)));
}
return mTabbedPane;
}
/**
* Split pane for channels table and channel details tabs.
*/
private JideSplitPane getSplitPane()
{
if(mSplitPane == null)
{
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
mSplitPane.setShowGripper(true);
}
return mSplitPane;
}
}

View File

@@ -95,7 +95,7 @@ public abstract class DecoderState extends AbstractDecoderState
/**
* Sets the current channel descriptor
*/
protected void setCurrentChannel(IChannelDescriptor channel)
public void setCurrentChannel(IChannelDescriptor channel)
{
mCurrentChannel = channel;
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -44,13 +44,12 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
@@ -91,10 +90,9 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
/**
* Constructs an instance
* @param channel configuration
* @param aliasModel for channel metadata and identifiers
* @param timeslots array of timeslot numbers to use
*/
public MultiChannelState(Channel channel, AliasModel aliasModel, int[] timeslots)
public MultiChannelState(AliasModel aliasModel, Channel channel, int[] timeslots)
{
super(channel);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -46,11 +46,10 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Channel state tracks the overall state of all processing modules and decoders configured for the channel and
@@ -96,7 +95,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache();
public SingleChannelState(Channel channel, AliasModel aliasModel)
public SingleChannelState(AliasModel aliasModel, Channel channel)
{
super(channel);
mChannelMetadata = new ChannelMetadata(aliasModel);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,21 +19,23 @@
package io.github.dsheirer.controller;
import com.jidesoft.swing.JideTabbedPane;
import io.github.dsheirer.audio.call.CallView;
import io.github.dsheirer.audio.playback.AudioPanel;
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelsView;
import io.github.dsheirer.audio.playbackfx.IPanelRevalidationRequestListener;
import io.github.dsheirer.channel.metadata.NowPlayingPanel;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.map.MapPanel;
import io.github.dsheirer.map.MapService;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.ui.TunerViewPanel;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
@@ -44,44 +46,50 @@ import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class ControllerPanel extends JPanel
public class ControllerPanel extends JPanel implements IPanelRevalidationRequestListener
{
private final static Logger mLog = LoggerFactory.getLogger(ControllerPanel.class);
private static final long serialVersionUID = 1L;
private int mSettingsTabIndex = -1;
@Resource
private AudioPanel mAudioPanel;
private NowPlayingPanel mNowPlayingPanel;
@Resource
private CallView mCallView;
@Resource
private MapPanel mMapPanel;
@Resource
private TunerViewPanel mTunerManagerPanel;
@Resource
private NowPlayingPanel mNowPlayingPanel;
@Resource
private AudioPlaybackChannelsView mAudioPlaybackChannelsView;
private JideTabbedPane mTabbedPane;
private JFXPanel mJFXCallViewPanel;
private JFXPanel mJFXAudioPlaybackViewPanel;
private int mPlaylistEditorTabIndex = -1;
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
TunerManager tunerManager, UserPreferences userPreferences)
/**
* Constructs an instance
*/
public ControllerPanel()
{
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
playlistManager.getAliasModel());
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
init();
}
private void init()
@PostConstruct
public void postConstruct()
{
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[shrink 0]0[grow,fill]0[]"));
add(mAudioPanel, "wrap");
// add(mAudioPanel, "wrap");
add(getAudioPlaybackViewPanel(), "wrap");
mAudioPlaybackChannelsView.setPanelRevalidationRequestListener(this);
mTabbedPane = new JideTabbedPane()
{
@Override
public void setSelectedIndex(int index)
{
if(index == mSettingsTabIndex)
if(index == mPlaylistEditorTabIndex)
{
MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest());
}
@@ -93,17 +101,72 @@ public class ControllerPanel extends JPanel
};
mTabbedPane.setFont(this.getFont());
mTabbedPane.setForeground(Color.BLACK);
mTabbedPane.addTab("Now Playing", mNowPlayingPanel);
mTabbedPane.addTab("Now Playing", getNowPlayingPanel());
mTabbedPane.addTab("Calls", getCallViewPanel());
mTabbedPane.addTab("Map", mMapPanel);
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
Icon gearIcon = IconFontSwing.buildIcon(FontAwesome.COG, 20, Color.DARK_GRAY);
mTabbedPane.addTab("Playlist Editor", gearIcon, new JLabel("Show Playlist Manager"));
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
mPlaylistEditorTabIndex = mTabbedPane.getTabCount() - 1;
//Set preferred size to influence the split between these panels
mTabbedPane.setPreferredSize(new Dimension(880, 500));
add(mTabbedPane, "wrap");
}
/**
* Implements listener interface to revalidate JavaFX panel when the audio channel view style has changed.
*/
@Override
public void revalidatePanel()
{
Platform.runLater(() ->
{
//Hack: this is very heavy-handed, however I couldn't find a way to get the JFXPanel to resize the height
//when the child content changes with the style of the audio playback panels. Nonetheless, this works.
Scene scene = getAudioPlaybackViewPanel().getScene();
getAudioPlaybackViewPanel().setScene(null);
getAudioPlaybackViewPanel().setScene(scene);
});
}
/**
* Now playing panel.
*/
public NowPlayingPanel getNowPlayingPanel()
{
return mNowPlayingPanel;
}
/**
* Call view panel wrapped in an JFXPanel. This is lazy constructed on the JavaFX platform thread.
* @return constructed JFXPanel.
*/
private JFXPanel getCallViewPanel()
{
createJFXPanels();
return mJFXCallViewPanel;
}
private JFXPanel getAudioPlaybackViewPanel()
{
createJFXPanels();
return mJFXAudioPlaybackViewPanel;
}
private void createJFXPanels()
{
if(mJFXCallViewPanel == null || mJFXAudioPlaybackViewPanel == null)
{
mJFXAudioPlaybackViewPanel = new JFXPanel();
mJFXCallViewPanel = new JFXPanel();
Platform.runLater(() -> {
mJFXCallViewPanel.setScene(new Scene(mCallView));
mJFXAudioPlaybackViewPanel.setScene(new Scene(mAudioPlaybackChannelsView));
});
}
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.controller.channel;
@@ -26,29 +23,32 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.springframework.stereotype.Component;
/**
* Channel Model
*/
@Component("channelModel")
public class ChannelModel implements Listener<ChannelEvent>
{
private ObservableList<Channel> mChannels = FXCollections.observableArrayList(Channel.extractor());
private ObservableList<Channel> mTrafficChannels = FXCollections.observableArrayList(Channel.extractor());
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
@Resource
private AliasModel mAliasModel;
public ChannelModel(AliasModel aliasModel)
/**
* Constructs an instance
*/
public ChannelModel()
{
mAliasModel = aliasModel;
//Register a listener to detect channel changes and broadcast change events to cause playlist save requests
ChannelListChangeListener changeListener = new ChannelListChangeListener();
mChannels.addListener(changeListener);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,19 +20,20 @@ package io.github.dsheirer.controller.channel;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.audio.AudioManager;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.channel.metadata.ChannelAndMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
import io.github.dsheirer.map.MapService;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.module.decode.DecoderFactory;
@@ -50,6 +51,8 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.GraphicsEnvironment;
import java.util.ArrayList;
import java.util.List;
@@ -63,6 +66,7 @@ import java.util.concurrent.locks.ReentrantLock;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Channel processing manager handles all starting and stopping of channel decoding. A processing chain is created
@@ -70,53 +74,49 @@ import org.slf4j.LoggerFactory;
* channel and protocol along with all logging and baseband or bitstream recording. Audio recording is handled outside
* of this class by the RecorderManager.
*/
@Component("channelProcessingManager")
public class ChannelProcessingManager implements Listener<ChannelEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelProcessingManager.class);
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
private Map<Channel,ProcessingChain> mProcessingChains = new ConcurrentHashMap<>();
private Lock mLock = new ReentrantLock();
private ChannelSourceEventErrorListener mSourceErrorListener = new ChannelSourceEventErrorListener();
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 TunerManager mTunerManager;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private List<Long> mLoggedFrequencies = new ArrayList<>();
private List<ScheduledFuture<?>> mDelayedChannelStartTasks = new ArrayList<>();
/**
* Constructs the channel processing manager
*
* @param channelMapModel containing channel maps defined by the user
* @param eventLogManager for adding event loggers to channels
* @param tunerManager 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,
TunerManager tunerManager, AliasModel aliasModel, UserPreferences userPreferences)
{
mChannelMapModel = channelMapModel;
mEventLogManager = eventLogManager;
mTunerManager = tunerManager;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mChannelMetadataModel = new ChannelMetadataModel();
}
@Resource
private AudioManager mAudioManager;
@Resource
private AliasModel mAliasModel;
@Resource
private ChannelMetadataModel mChannelMetadataModel;
@Resource
private DecoderFactory mDecoderFactory;
@Resource
private EventLogManager mEventLogManager;
@Resource
private MapService mMapService;
@Resource
private TunerManager mTunerManager;
@Resource
private UserPreferences mUserPreferences;
/**
* Channel metadata model containing metadata for each channel or channel time-slice that is currently processing.
* Constructs the channel processing manager
*/
public ChannelMetadataModel getChannelMetadataModel()
public ChannelProcessingManager()
{
return mChannelMetadataModel;
}
@PostConstruct
public void postConstruct()
{
addAudioSegmentListener(mAudioManager);
addDecodeEventListener(mMapService);
}
/**
@@ -365,7 +365,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
* @param request containing channel and other details
* @throws ChannelException if a source is not available for the channel
*/
private void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
private synchronized void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
{
Channel channel = request.getChannel();
@@ -398,7 +398,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
throw new ChannelException("No Tuner Available");
}
ProcessingChain processingChain = new ProcessingChain(channel, mAliasModel);
ProcessingChain processingChain = new ProcessingChain(mAliasModel, channel);
//Certain decoders aggregate the decode events in the parent channel that also includes any events produced
//by the traffic channels. Establish listener registration depending on if this channel is a traffic channel
@@ -441,8 +441,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
processingChain.addFrequencyChangeListener(channel);
/* Processing Modules */
List<Module> modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences,
request.getTrafficChannelManager());
List<Module> modules = mDecoderFactory.getModules(channel, request.getTrafficChannelManager(),
request.getChannelDescriptor());
processingChain.addModules(modules);
//Post preload data from the request to the event bus. Modules that can handle preload data will annotate
@@ -560,7 +560,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
{
added = true;
mProcessingChains.put(channel, processingChain);
getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
mChannelMetadataModel.add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
}
}
finally
@@ -590,7 +590,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
{
for(ChannelMetadata channelMetadata: removed.getChannelState().getChannelMetadata())
{
getChannelMetadataModel().remove(channelMetadata);
mChannelMetadataModel.remove(channelMetadata);
}
}
}

View File

@@ -19,20 +19,27 @@
package io.github.dsheirer.controller.channel;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
public class ChannelSelectionManager implements Listener<ChannelEvent>
{
private Listener<ChannelEvent> mChannelEventListener;
@Resource
private ChannelModel mChannelModel;
private Channel mSelectedChannel = null;
/**
* Manages channel selection state to ensure that only one channel is ever
* in a selected state.
*/
public ChannelSelectionManager(ChannelModel channelModel)
public ChannelSelectionManager()
{
mChannelEventListener = channelModel;
channelModel.addListener(ChannelSelectionManager.this);
}
@PostConstruct
public void postConstruct()
{
mChannelModel.addListener(ChannelSelectionManager.this);
}
@Override
@@ -44,13 +51,13 @@ public class ChannelSelectionManager implements Listener<ChannelEvent>
if(mSelectedChannel != null)
{
mSelectedChannel.setSelected(false);
mChannelEventListener.receive(new ChannelEvent(mSelectedChannel, ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
mChannelModel.receive(new ChannelEvent(mSelectedChannel, ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
mSelectedChannel = null;
}
mSelectedChannel = event.getChannel();
mSelectedChannel.setSelected(true);
mChannelEventListener.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
mChannelModel.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
break;
case NOTIFICATION_PROCESSING_STOP:
case REQUEST_DESELECT:
@@ -59,7 +66,7 @@ public class ChannelSelectionManager implements Listener<ChannelEvent>
{
mSelectedChannel.setSelected(false);
mSelectedChannel = null;
mChannelEventListener.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
mChannelModel.receive(new ChannelEvent(event.getChannel(), ChannelEvent.Event.NOTIFICATION_SELECTION_CHANGE));
}
break;
default:

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.controller.channel.map;
@@ -25,15 +22,17 @@ package io.github.dsheirer.controller.channel.map;
import io.github.dsheirer.controller.channel.map.ChannelMapEvent.Event;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.swing.AbstractListModel;
import java.util.ArrayList;
import java.util.List;
@Component("channelMapModel")
public class ChannelMapModel extends AbstractListModel<ChannelMap>
{
private static final long serialVersionUID = 1L;

View File

@@ -68,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
//Sized to process 1/20th batch of 152 buffers a second, at 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(152 / 20, 25);
//Sized to process 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(25);
private FloatFFT_1D mFFT;
private float[] mInlineSamples;
private float[] mInlineFilter;
@@ -406,9 +406,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
{
public IFFTProcessorDispatcher(int batchSize, long interval)
public IFFTProcessorDispatcher(long interval)
{
super("sdrtrunk polyphase ifft processor", batchSize, interval);
super("sdrtrunk polyphase ifft processor", interval);
//We create a listener interface to receive the batched channel results arrays from the scheduled thread pool
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each

View File

@@ -110,7 +110,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 50, 10);
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
mBufferDispatcher.setListener(mNativeBufferReceiver);
}

View File

@@ -50,8 +50,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
mInputChannelCount = inputChannelCount;
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
mHeartbeatManager = heartbeatManager;
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel", (int)(sampleRate / 10),
50, mHeartbeatManager);
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel",50, mHeartbeatManager);
mChannelResultsDispatcher.setListener(floats -> {
try
{

View File

@@ -1,18 +1,21 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage;
@@ -177,6 +180,28 @@ public class CRCDMR
return 2;
}
/**
* Calculates the residual CRC value using a mask of zero which should identify the mask value when an alternate
* mask value is employed.
* @param message to be checked
* @param messageStart location
* @param crcStart location
* @return residual CRC check value.
*/
public static int calculateResidual(CorrectedBinaryMessage message, int messageStart, int crcStart)
{
int calculated = 0; //Starting value
/* Iterate the set bits and XOR running checksum with lookup value */
for(int i = message.nextSetBit(messageStart); i >= messageStart && i < crcStart; i = message.nextSetBit(i + 1))
{
calculated ^= CCITT_80_CHECKSUMS[i - messageStart];
}
int checksum = getIntChecksum(message, crcStart, 16);
return calculated ^ checksum;
}
/**
* Error detection for CRC-9 protected Confirmed Packet Data blocks. These
* data blocks have a slightly complicated structure because the checksum

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,23 +14,29 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.eventbus;
import com.google.common.eventbus.EventBus;
import org.springframework.stereotype.Component;
/**
* System wide event bus for dispatching/broadcasting system wide events or objects
*/
@Component("myEventBus")
public class MyEventBus
{
private static final EventBus GLOBAL_EVENT_BUS = new EventBus();
//TODO: this is primarily used by the Preference Service ... move this event bus to the Preference Service
public static EventBus getGlobalEventBus()
{
return GLOBAL_EVENT_BUS;
}
public EventBus getEventBus()
{
return MyEventBus.getGlobalEventBus();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,8 +20,8 @@
package io.github.dsheirer.gui;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.controller.channel.map.ChannelMap;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.controller.channel.map.ChannelRange;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.icon.IconManager;
@@ -37,26 +37,30 @@ import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.RecordingViewer;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.jmbe.JmbeEditor;
import io.github.dsheirer.jmbe.JmbeEditorRequest;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.monitor.ResourceMonitor;
import io.github.dsheirer.monitor.StatusBox;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.stage.Stage;
import jiconfont.javafx.IconFontFX;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Java FX window manager. Handles all secondary Java FX windows that are used within this primarily
* Swing application.
*/
@Component("javaFxWindowManager")
public class JavaFxWindowManager extends Application
{
private final static Logger mLog = LoggerFactory.getLogger(JavaFxWindowManager.class);
@@ -65,7 +69,6 @@ public class JavaFxWindowManager extends Application
public static final String ICON_MANAGER = "iconmanager";
public static final String PLAYLIST_EDITOR = "playlist";
public static final String USER_PREFERENCES_EDITOR = "preferences";
public static final String STAGE_MONITOR_KEY_CALIBRATION_DIALOG = "calibration.dialog";
public static final String STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR = "channel.map";
public static final String STAGE_MONITOR_KEY_RECORDING_VIEWER = "recording.viewer";
public static final String STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR = "icon.manager";
@@ -73,60 +76,64 @@ public class JavaFxWindowManager extends Application
public static final String STAGE_MONITOR_KEY_PLAYLIST_EDITOR = "playlist";
public static final String STAGE_MONITOR_KEY_USER_PREFERENCES_EDITOR = "user.preferences";
private JFXPanel mJFXPanel;
@Resource
private ChannelMapEditor mChannelMapEditor;
@Resource
private ChannelMapModel mChannelMapModel;
@Resource
private IconManager mIconManager;
@Resource
private JmbeEditor mJmbeEditor;
@Resource
private PlaylistEditor mPlaylistEditor;
private PlaylistManager mPlaylistManager;
private TunerManager mTunerManager;
@Resource
private UserPreferences mUserPreferences;
@Resource
private UserPreferencesEditor mUserPreferencesEditor;
private RecordingViewer mRecordingViewer;
private JFXPanel mStatusPanel;
private RecordingViewer mRecordingViewer;
private Stage mChannelMapStage;
private Stage mIconManagerStage;
private Stage mJmbeEditorStage;
private Stage mPlaylistStage;
private Stage mUserPreferencesStage;
private Stage mRecordingViewerStage;
private List<Stage> mStages = new ArrayList<>();
/**
* Constructs an instance. Note: this constructor is used for Swing applications.
*/
public JavaFxWindowManager(UserPreferences userPreferences, TunerManager tunerManager, PlaylistManager playlistManager)
{
mUserPreferences = userPreferences;
mTunerManager = tunerManager;
mPlaylistManager = playlistManager;
setup();
}
/**
* Constructs an instance. Note: this constructor is used for standalone JavaFX application testing
*/
public JavaFxWindowManager()
{
mUserPreferences = new UserPreferences();
AliasModel aliasModel = new AliasModel();
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
mTunerManager = new TunerManager(mUserPreferences);
mTunerManager.start();
mPlaylistManager = new PlaylistManager(mUserPreferences, mTunerManager, aliasModel, eventLogManager, new IconModel());
mPlaylistManager.init();
setup();
}
private void setup()
@PostConstruct
private void postConstruct()
{
//Register this class to receive events via each method annotated with @Subscribe
MyEventBus.getGlobalEventBus().register(this);
}
//Register JavaFX icon fonts
IconFontFX.register(jiconfont.icons.font_awesome.FontAwesome.getIconFont());
/**
* Creates or accesses the JavaFX status panel, used by the main application GUI.
* @param resourceMonitor for statistics
* @return JFXPanel accessible on Swing thread that delegates JavaFX scene creation to the FX event thread.
*/
public JFXPanel getStatusPanel(ResourceMonitor resourceMonitor)
{
if(mStatusPanel == null)
{
mStatusPanel = new JFXPanel();
createJFXPanel();
//JFXPanel has to be populated on the FX event thread
Platform.runLater(() -> {
Scene scene = new Scene(new StatusBox(resourceMonitor));
mStatusPanel.setScene(scene);
});
}
return mStatusPanel;
}
/**
@@ -134,8 +141,6 @@ public class JavaFxWindowManager extends Application
*/
private void execute(Runnable runnable)
{
createJFXPanel();
if(Platform.isFxApplicationThread())
{
runnable.run();
@@ -146,51 +151,38 @@ public class JavaFxWindowManager extends Application
}
}
/**
* Creates a JavaFX panel for Swing application compatibility
*/
private void createJFXPanel()
{
if(mJFXPanel == null)
{
mJFXPanel = new JFXPanel();
Platform.setImplicitExit(false);
}
}
/**
* Removes monitoring for all JavaFX stages and shuts down the FX thread, killing all FX windows.
*/
public void shutdown()
{
MyEventBus.getGlobalEventBus().unregister(this);
mUserPreferences.getJavaFxPreferences().clearStageMonitors();
if(mChannelMapStage != null)
Platform.runLater(() ->
{
mUserPreferences.getJavaFxPreferences().unmonitor(mChannelMapStage);
}
Iterator<Stage> it = mStages.iterator();
if(mIconManagerStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mIconManagerStage);
}
while(it.hasNext())
{
try
{
it.next().close();
}
catch(Throwable t)
{
//Do nothing ... we're shutting down
}
if(mPlaylistStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mPlaylistStage);
}
it.remove();
}
if(mUserPreferencesStage != null)
{
mUserPreferences.getJavaFxPreferences().unmonitor(mUserPreferencesStage);
}
Platform.exit();
Platform.exit();
});
}
public CalibrationDialog getCalibrationDialog(UserPreferences userPreferences)
{
createJFXPanel();
return new CalibrationDialog(userPreferences);
}
@@ -201,11 +193,11 @@ public class JavaFxWindowManager extends Application
{
if(mRecordingViewerStage == null)
{
createJFXPanel();
Scene scene = new Scene(getRecordingViewer(), 1100, 800);
mRecordingViewerStage = new Stage();
mRecordingViewerStage.setTitle("sdrtrunk - Message Recording Viewer (.bits)");
mRecordingViewerStage.setScene(scene);
mStages.add(mRecordingViewerStage);
mUserPreferences.getJavaFxPreferences().monitor(mRecordingViewerStage, STAGE_MONITOR_KEY_RECORDING_VIEWER);
}
@@ -226,11 +218,11 @@ public class JavaFxWindowManager extends Application
{
if(mIconManagerStage == null)
{
createJFXPanel();
Scene scene = new Scene(getIconManager(), 500, 500);
mIconManagerStage = new Stage();
mIconManagerStage.setTitle("sdrtrunk - Icon Manager");
mIconManagerStage.setScene(scene);
mStages.add(mIconManagerStage);
mUserPreferences.getJavaFxPreferences().monitor(mIconManagerStage, STAGE_MONITOR_KEY_ICON_MANAGER_EDITOR);
}
@@ -239,11 +231,6 @@ public class JavaFxWindowManager extends Application
public IconManager getIconManager()
{
if(mIconManager == null)
{
mIconManager = new IconManager(mPlaylistManager.getIconModel());
}
return mIconManager;
}
@@ -264,9 +251,7 @@ public class JavaFxWindowManager extends Application
else
{
execute(() -> {
getJmbeEditorStage().show();
getJmbeEditorStage().requestFocus();
getJmbeEditorStage().toFront();
restoreStage(getJmbeEditorStage());
getJmbeEditor().process(request);
});
}
@@ -276,11 +261,11 @@ public class JavaFxWindowManager extends Application
{
if(mJmbeEditorStage == null)
{
createJFXPanel();
Scene scene = new Scene(getJmbeEditor(), 650, 650);
mJmbeEditorStage = new Stage();
mJmbeEditorStage.setTitle("sdrtrunk - JMBE Library Updater");
mJmbeEditorStage.setScene(scene);
mStages.add(mJmbeEditorStage);
mUserPreferences.getJavaFxPreferences().monitor(mJmbeEditorStage, STAGE_MONITOR_KEY_JMBE_EDITOR);
}
@@ -289,11 +274,6 @@ public class JavaFxWindowManager extends Application
public JmbeEditor getJmbeEditor()
{
if(mJmbeEditor == null)
{
mJmbeEditor = new JmbeEditor(mUserPreferences);
}
return mJmbeEditor;
}
@@ -302,11 +282,6 @@ public class JavaFxWindowManager extends Application
*/
public PlaylistEditor getPlaylistEditor()
{
if(mPlaylistEditor == null)
{
mPlaylistEditor = new PlaylistEditor(mPlaylistManager, mTunerManager, mUserPreferences);
}
return mPlaylistEditor;
}
@@ -317,11 +292,11 @@ public class JavaFxWindowManager extends Application
{
if(mPlaylistStage == null)
{
createJFXPanel();
Scene scene = new Scene(getPlaylistEditor(), 1000, 750);
mPlaylistStage = new Stage();
mPlaylistStage.setTitle("sdrtrunk - Playlist Editor");
mPlaylistStage.setScene(scene);
mStages.add(mPlaylistStage);
mUserPreferences.getJavaFxPreferences().monitor(mPlaylistStage, STAGE_MONITOR_KEY_PLAYLIST_EDITOR);
}
@@ -337,9 +312,7 @@ public class JavaFxWindowManager extends Application
execute(() -> {
try
{
getPlaylistStage().show();
getPlaylistStage().requestFocus();
getPlaylistStage().toFront();
restoreStage(getPlaylistStage());
getPlaylistEditor().process(request);
}
catch(Throwable t)
@@ -354,11 +327,6 @@ public class JavaFxWindowManager extends Application
*/
private UserPreferencesEditor getUserPreferencesEditor()
{
if(mUserPreferencesEditor == null)
{
mUserPreferencesEditor = new UserPreferencesEditor(mUserPreferences);
}
return mUserPreferencesEditor;
}
@@ -369,11 +337,11 @@ public class JavaFxWindowManager extends Application
{
if(mUserPreferencesStage == null)
{
createJFXPanel();
Scene scene = new Scene(getUserPreferencesEditor(), 900, 500);
mUserPreferencesStage = new Stage();
mUserPreferencesStage.setTitle("sdrtrunk - User Preferences");
mUserPreferencesStage.setScene(scene);
mStages.add(mUserPreferencesStage);
mUserPreferences.getJavaFxPreferences().monitor(mUserPreferencesStage, STAGE_MONITOR_KEY_USER_PREFERENCES_EDITOR);
}
@@ -387,9 +355,7 @@ public class JavaFxWindowManager extends Application
public void process(final ViewUserPreferenceEditorRequest request)
{
execute(() -> {
getUserPreferencesStage().show();
getUserPreferencesStage().requestFocus();
getUserPreferencesStage().toFront();
restoreStage(getUserPreferencesStage());
getUserPreferencesEditor().process(request);
});
}
@@ -399,11 +365,6 @@ public class JavaFxWindowManager extends Application
*/
private ChannelMapEditor getChannelMapEditor()
{
if(mChannelMapEditor == null)
{
mChannelMapEditor = new ChannelMapEditor(mPlaylistManager.getChannelMapModel());
}
return mChannelMapEditor;
}
@@ -414,11 +375,11 @@ public class JavaFxWindowManager extends Application
{
if(mChannelMapStage == null)
{
createJFXPanel();
Scene scene = new Scene(getChannelMapEditor(), 500, 500);
mChannelMapStage = new Stage();
mChannelMapStage.setTitle("sdrtrunk - Channel Map Editor");
mChannelMapStage.setScene(scene);
mStages.add(mChannelMapStage);
mUserPreferences.getJavaFxPreferences().monitor(mChannelMapStage, STAGE_MONITOR_KEY_CHANNEL_MAP_EDITOR);
}
@@ -428,11 +389,7 @@ public class JavaFxWindowManager extends Application
@Subscribe
public void process(final ViewIconManagerRequest request)
{
execute(() -> {
getIconManagerStage().show();
getIconManagerStage().requestFocus();
getIconManagerStage().toFront();
});
execute(() -> restoreStage(getIconManagerStage()));
}
/**
@@ -442,9 +399,7 @@ public class JavaFxWindowManager extends Application
public void process(final ViewChannelMapEditorRequest request)
{
execute(() -> {
getChannelMapStage().show();
getChannelMapStage().requestFocus();
getChannelMapStage().toFront();
restoreStage(getChannelMapStage());
getChannelMapEditor().process(request);
});
}
@@ -455,11 +410,22 @@ public class JavaFxWindowManager extends Application
@Subscribe
public void process(final ViewRecordingViewerRequest request)
{
execute(() -> {
getRecordingViewerStage().show();
getRecordingViewerStage().requestFocus();
getRecordingViewerStage().toFront();
});
execute(() -> restoreStage(getRecordingViewerStage()));
}
/**
* Restores the stage to previous size and location.
* @param stage to restore.
*/
private void restoreStage(Stage stage)
{
stage.setIconified(false);
stage.show();
stage.requestFocus();
stage.toFront();
stage.setMinHeight(stage.getHeight());
stage.setMinWidth(stage.getWidth());
// System.out.println("Stage [" + stage.getTitle() + "] restored - width[" + stage.getWidth() + "] height [" + stage.getHeight() + "]");
}
@Override
@@ -486,14 +452,14 @@ public class JavaFxWindowManager extends Application
channelMap1.addRange(new ChannelRange(200,299,160000000, 25000));
channelMap1.addRange(new ChannelRange(300,399,170000000, 12500));
channelMap1.addRange(new ChannelRange(400,499,180000000, 25000));
mPlaylistManager.getChannelMapModel().addChannelMap(channelMap1);
mChannelMapModel.addChannelMap(channelMap1);
ChannelMap channelMap2 = new ChannelMap("Test Map 2");
channelMap2.addRange(new ChannelRange(1,199,450000000, 12500));
channelMap2.addRange(new ChannelRange(200,299,460000000, 25000));
channelMap2.addRange(new ChannelRange(300,399,470000000, 12500));
channelMap2.addRange(new ChannelRange(400,499,480000000, 25000));
mPlaylistManager.getChannelMapModel().addChannelMap(channelMap2);
mChannelMapModel.addChannelMap(channelMap2);
valid = true;
process(new ViewChannelMapEditorRequest());
break;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,723 +18,154 @@
*/
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.DuplicateCallDetector;
import io.github.dsheirer.audio.broadcast.AudioStreamingManager;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.audio.playback.AudioPlaybackManager;
import io.github.dsheirer.controller.ControllerPanel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelException;
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
import io.github.dsheirer.gui.preference.CalibrateRequest;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.log.ApplicationLog;
import io.github.dsheirer.map.MapService;
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.AudioRecordingManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.tuner.Tuner;
import io.github.dsheirer.source.tuner.TunerEvent;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
import io.github.dsheirer.spectrum.ClearTunerMenuItem;
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.Dimension;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import javax.imageio.ImageIO;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.plaf.metal.MetalLookAndFeel;
public class SDRTrunk implements Listener<TunerEvent>
@SpringBootApplication(scanBasePackages = {"io.github.dsheirer"})
@EnableJpaRepositories("io.github.dsheirer.audio.call")
@EntityScan("io.github.dsheirer.audio.call")
public class SDRTrunk
{
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
private static final String PROPERTY_BROADCAST_STATUS_VISIBLE = "main.broadcast.status.visible";
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
private boolean mBroadcastStatusVisible;
private AudioRecordingManager mAudioRecordingManager;
private AudioStreamingManager mAudioStreamingManager;
private BroadcastStatusPanel mBroadcastStatusPanel;
private ControllerPanel mControllerPanel;
private IconModel mIconModel = new IconModel();
private PlaylistManager mPlaylistManager;
private SettingsManager mSettingsManager;
private SpectralDisplayPanel mSpectralPanel;
private JFrame mMainGui;
private JideSplitPane mSplitPane;
private JavaFxWindowManager mJavaFxWindowManager;
private UserPreferences mUserPreferences = new UserPreferences();
private TunerManager mTunerManager;
@Resource
private ApplicationLog mApplicationLog;
@Resource
private AudioRecordingManager mAudioRecordingManager;
@Resource
private ChannelModel mChannelModel;
@Resource
private ChannelProcessingManager mChannelProcessingManager;
@Resource
private TunerManager mTunerManager;
@Resource
private UserPreferences mUserPreferences;
@Resource //SpringUiFactory does not instantiate this instance when headless=true
private SDRTrunkUI mSDRTrunkUI;
private String mTitle;
/**
* Constructs an instance of the SDRTrunk application
*/
public SDRTrunk()
{
if(!GraphicsEnvironment.isHeadless())
{
mMainGui = new JFrame();
}
}
mApplicationLog = new ApplicationLog(mUserPreferences);
mApplicationLog.start();
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
{
try
{
UIManager.setLookAndFeel(MetalLookAndFeel.class.getName());
LookAndFeelFactory.installJideExtension();
}
catch(Exception e)
{
mLog.error("Error trying to set Metal look and feel for OS [" + operatingSystem + "]");
}
}
//Setup the application home directory
Path home = getHomePath();
@PostConstruct
public void postConstruct()
{
ThreadPool.logSettings();
mLog.info("Home path: " + home.toString());
//Load properties file
if(home != null)
{
loadProperties(home);
}
//Log current properties setting
SystemProperties.getInstance().logCurrentSettings();
//Register FontAwesome so we can use the fonts in Swing windows
IconFontSwing.register(FontAwesome.getIconFont());
mTunerManager = new TunerManager(mUserPreferences);
mTunerManager.start();
mSettingsManager = new SettingsManager();
AliasModel aliasModel = new AliasModel();
EventLogManager eventLogManager = new EventLogManager(aliasModel, mUserPreferences);
mPlaylistManager = new PlaylistManager(mUserPreferences, mTunerManager, aliasModel, eventLogManager, mIconModel);
if(!GraphicsEnvironment.isHeadless())
{
mJavaFxWindowManager = new JavaFxWindowManager(mUserPreferences, mTunerManager, mPlaylistManager);
}
CalibrationManager calibrationManager = CalibrationManager.getInstance(mUserPreferences);
final boolean calibrating = !calibrationManager.isCalibrated() &&
!mUserPreferences.getVectorCalibrationPreference().isHideCalibrationDialog();
new ChannelSelectionManager(mPlaylistManager.getChannelModel());
AudioPlaybackManager audioPlaybackManager = new AudioPlaybackManager(mUserPreferences);
mAudioRecordingManager = new AudioRecordingManager(mUserPreferences);
mAudioRecordingManager.start();
mAudioStreamingManager = new AudioStreamingManager(mPlaylistManager.getBroadcastModel(), BroadcastFormat.MP3,
mUserPreferences);
mAudioStreamingManager.start();
DuplicateCallDetector duplicateCallDetector = new DuplicateCallDetector(mUserPreferences);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(duplicateCallDetector);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(audioPlaybackManager);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioRecordingManager);
mPlaylistManager.getChannelProcessingManager().addAudioSegmentListener(mAudioStreamingManager);
MapService mapService = new MapService(mIconModel);
mPlaylistManager.getChannelProcessingManager().addDecodeEventListener(mapService);
if(!GraphicsEnvironment.isHeadless())
{
mControllerPanel = new ControllerPanel(mPlaylistManager, audioPlaybackManager, mIconModel, mapService,
mSettingsManager, mTunerManager, mUserPreferences);
}
mSpectralPanel = new SpectralDisplayPanel(mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
TunerSpectralDisplayManager tunerSpectralDisplayManager = new TunerSpectralDisplayManager(mSpectralPanel,
mPlaylistManager, mSettingsManager, mTunerManager.getDiscoveredTunerModel());
mTunerManager.getDiscoveredTunerModel().addListener(tunerSpectralDisplayManager);
mTunerManager.getDiscoveredTunerModel().addListener(this);
mPlaylistManager.init();
if(GraphicsEnvironment.isHeadless())
{
mLog.info("starting main application headless");
mLog.info("starting main application in headless mode");
autoStartChannels();
}
else
{
mLog.info("starting main application gui");
//Initialize the GUI
initGUI();
}
//Start the gui
EventQueue.invokeLater(() -> {
try
mLog.info("starting main application with gui");
if(mSDRTrunkUI != null)
{
if(!GraphicsEnvironment.isHeadless())
{
mMainGui.setVisible(true);
Tuner tuner = tunerSpectralDisplayManager.showFirstTuner();
if(tuner != null)
{
updateTitle(tuner.getPreferredName());
}
}
if(calibrating && !GraphicsEnvironment.isHeadless())
{
Platform.runLater(() ->
{
CalibrationDialog calibrationDialog = mJavaFxWindowManager.getCalibrationDialog(mUserPreferences);
Optional<ButtonType> calibrate = calibrationDialog.showAndWait();
if(calibrate.isPresent() && calibrate.get().getText().equals("Calibrate"))
{
//Request focus and execute calibration
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.VECTOR_CALIBRATION));
MyEventBus.getGlobalEventBus().post(new CalibrateRequest());
}
else
{
autoStartChannels();
}
});
}
else
{
autoStartChannels();
}
}
catch(Exception e)
{
e.printStackTrace();
}
});
}
/**
* Shows a dialog that lists the channels that have been designated for auto-start, sorted by auto-start order and
* allows the user to start now, cancel, or allow the timer to expire and then start the channels. The dialog will
* only show if there are one ore more channels designated for auto-start.
*/
private void autoStartChannels()
{
List<Channel> channels = mPlaylistManager.getChannelModel().getAutoStartChannels();
if(channels.size() > 0)
{
if(GraphicsEnvironment.isHeadless())
{
for(Channel channel: channels)
{
try
{
mLog.info("Auto-starting channel " + channel.getName());
mPlaylistManager.getChannelProcessingManager().start(channel);
}
catch(ChannelException ce)
{
mLog.error("Channel: " + channel.getName() + " auto-start failed: " + ce.getMessage());
}
}
EventQueue.invokeLater(() -> mSDRTrunkUI.setVisible(true));
}
else
{
new ChannelAutoStartFrame(mPlaylistManager.getChannelProcessingManager(), channels, mUserPreferences);
mLog.error("SDRTrunk user interface is null - can't start UI");
}
}
}
/**
* Initialize the contents of the frame.
* Starts processing any channel configurations that are designated to auto-start
*/
private void initGUI()
private void autoStartChannels()
{
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
/**
* Setup main JFrame window
*/
mTitle = SystemProperties.getInstance().getApplicationName();
mMainGui.setTitle(mTitle);
Point location = mUserPreferences.getSwingPreference().getLocation(WINDOW_FRAME_IDENTIFIER);
if(location != null)
List<Channel> channels = mChannelModel.getAutoStartChannels();
for(Channel channel: channels)
{
mMainGui.setLocation(location);
}
else
{
mMainGui.setLocationRelativeTo(null);
}
mMainGui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mMainGui.addWindowListener(new ShutdownMonitor());
Dimension dimension = mUserPreferences.getSwingPreference().getDimension(WINDOW_FRAME_IDENTIFIER);
mSpectralPanel.setPreferredSize(new Dimension(1280, 300));
mControllerPanel.setPreferredSize(new Dimension(1280, 500));
if(dimension != null)
{
Dimension spectral = mUserPreferences.getSwingPreference().getDimension(SPECTRAL_PANEL_IDENTIFIER);
if(spectral != null)
{
mSpectralPanel.setSize(spectral);
}
Dimension controller = mUserPreferences.getSwingPreference().getDimension(CONTROLLER_PANEL_IDENTIFIER);
if(controller != null)
{
mControllerPanel.setSize(controller);
}
mMainGui.setSize(dimension);
if(mUserPreferences.getSwingPreference().getMaximized(WINDOW_FRAME_IDENTIFIER, false))
{
mMainGui.setExtendedState(JFrame.MAXIMIZED_BOTH);
}
}
else
{
mMainGui.setSize(new Dimension(1280, 800));
}
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
mSplitPane.setDividerSize(5);
mSplitPane.add(mSpectralPanel);
mSplitPane.add(mControllerPanel);
mBroadcastStatusVisible = SystemProperties.getInstance().get(PROPERTY_BROADCAST_STATUS_VISIBLE, false);
//Show broadcast status panel when user requests - disabled by default
if(mBroadcastStatusVisible)
{
mSplitPane.add(getBroadcastStatusPanel());
}
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
/**
* Menu items
*/
JMenuBar menuBar = new JMenuBar();
mMainGui.setJMenuBar(menuBar);
JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
JMenuItem exitMenu = new JMenuItem("Exit");
exitMenu.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
processShutdown();
System.exit(0);
}
}
);
fileMenu.add(exitMenu);
JMenu viewMenu = new JMenu("View");
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
viewMenu.add(viewPlaylistItem);
viewMenu.add(new JSeparator());
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
viewMenu.add(recordingViewerMenu);
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
settingsMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
viewMenu.add(settingsMenu);
JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
logFilesMenu.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
try
{
Desktop.getDesktop().open(getHomePath().toFile());
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " +
getHomePath().toString(),
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
}
});
viewMenu.add(logFilesMenu);
JMenuItem preferencesItem = new JMenuItem("User Preferences");
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
viewMenu.add(preferencesItem);
viewMenu.add(new JSeparator());
viewMenu.add(new TunersMenu());
viewMenu.add(new JSeparator());
viewMenu.add(new ClearTunerMenuItem(mSpectralPanel));
viewMenu.add(new BroadcastStatusVisibleMenuItem(mControllerPanel));
menuBar.add(viewMenu);
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
screenCaptureItem.addActionListener(arg0 -> {
try
{
Robot robot = new Robot();
final BufferedImage image = robot.createScreenCapture(mMainGui.getBounds());
String filename = TimeStamp.getTimeStamp("_") + "_screen_capture.png";
final Path captureFile = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().resolve(filename);
EventQueue.invokeLater(() -> {
try
{
ImageIO.write(image, "png", captureFile.toFile());
}
catch(IOException e)
{
mLog.error("Couldn't write screen capture to file [" + captureFile.toString() + "]", e);
}
});
mLog.info("Auto-starting channel " + channel.getName());
mChannelProcessingManager.start(channel);
}
catch(AWTException e)
catch(ChannelException ce)
{
mLog.error("Exception while taking screen capture", e);
mLog.error("Channel: " + channel.getName() + " auto-start failed: " + ce.getMessage());
}
});
menuBar.add(screenCaptureItem);
}
}
/**
* Performs shutdown operations
*/
private void processShutdown()
@PreDestroy
public void preDestroy()
{
mLog.info("Application shutdown started ...");
mUserPreferences.getSwingPreference().setLocation(WINDOW_FRAME_IDENTIFIER, mMainGui.getLocation());
mUserPreferences.getSwingPreference().setDimension(WINDOW_FRAME_IDENTIFIER, mMainGui.getSize());
mUserPreferences.getSwingPreference().setMaximized(WINDOW_FRAME_IDENTIFIER,
(mMainGui.getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH);
mUserPreferences.getSwingPreference().setDimension(SPECTRAL_PANEL_IDENTIFIER, mSpectralPanel.getSize());
mUserPreferences.getSwingPreference().setDimension(CONTROLLER_PANEL_IDENTIFIER, mControllerPanel.getSize());
mJavaFxWindowManager.shutdown();
mLog.info("Stopping channels ...");
mPlaylistManager.getChannelProcessingManager().shutdown();
mChannelProcessingManager.shutdown();
mAudioRecordingManager.stop();
mLog.info("Stopping spectral display ...");
mSpectralPanel.clearTuner();
mLog.info("Stopping tuners ...");
mTunerManager.stop();
mLog.info("Shutdown complete.");
mApplicationLog.stop();
}
/**
* Lazy constructor for broadcast status panel
* Monitors the SDRTrunkUI window for shutdown so that we can terminate the overall SDRTrunk application.
*/
private BroadcastStatusPanel getBroadcastStatusPanel()
{
if(mBroadcastStatusPanel == null)
{
mBroadcastStatusPanel = new BroadcastStatusPanel(mPlaylistManager.getBroadcastModel(), mUserPreferences,
"application.broadcast.status.panel");
mBroadcastStatusPanel.setPreferredSize(new Dimension(880, 70));
mBroadcastStatusPanel.getTable().setEnabled(false);
}
return mBroadcastStatusPanel;
}
/**
* Toggles visibility of the broadcast channels status panel at the bottom of the controller panel
*/
private void toggleBroadcastStatusPanelVisibility()
{
mBroadcastStatusVisible = !mBroadcastStatusVisible;
EventQueue.invokeLater(() -> {
if(mBroadcastStatusVisible)
{
mSplitPane.add(getBroadcastStatusPanel());
}
else
{
mSplitPane.remove(getBroadcastStatusPanel());
}
mMainGui.revalidate();
});
SystemProperties.getInstance().set(PROPERTY_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
}
/**
* Loads the application properties file from the user's home directory,
* creating the properties file for the first-time, if necessary
*/
private void loadProperties(Path homePath)
{
Path propsPath = homePath.resolve("SDRTrunk.properties");
if(!Files.exists(propsPath))
{
try
{
mLog.info("SDRTrunk - creating application properties file [" +
propsPath.toAbsolutePath() + "]");
Files.createFile(propsPath);
}
catch(IOException e)
{
mLog.error("SDRTrunk - couldn't create application properties "
+ "file [" + propsPath.toAbsolutePath(), e);
}
}
if(Files.exists(propsPath))
{
SystemProperties.getInstance().load(propsPath);
}
else
{
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
"application properties file");
}
}
/**
* Gets (or creates) the SDRTRunk application home directory.
*
* Note: the user can change this setting to allow log files and other
* files to reside elsewhere on the file system.
*/
private Path getHomePath()
{
Path homePath = FileSystems.getDefault()
.getPath(System.getProperty("user.home"), "SDRTrunk");
if(!Files.exists(homePath))
{
try
{
Files.createDirectory(homePath);
mLog.info("SDRTrunk - created application home directory [" +
homePath.toString() + "]");
}
catch(Exception e)
{
homePath = null;
mLog.error("SDRTrunk: exception while creating SDRTrunk home " +
"directory in the user's home directory", e);
}
}
return homePath;
}
@Override
public void receive(TunerEvent event)
{
switch(event.getEvent())
{
case REQUEST_MAIN_SPECTRAL_DISPLAY:
updateTitle(event.getTuner().getPreferredName());
break;
case REQUEST_CLEAR_MAIN_SPECTRAL_DISPLAY:
updateTitle(null);
break;
case NOTIFICATION_SHUTTING_DOWN:
Tuner currentTuner = mSpectralPanel.getTuner();
if(event.hasTuner() && event.getTuner().equals(currentTuner) || currentTuner == null)
{
updateTitle(null);
}
break;
}
}
/**
* Updates the title bar with the tuner name
* @param tunerName optional
*/
private void updateTitle(String tunerName)
{
if(tunerName != null)
{
mMainGui.setTitle(mTitle + " - " + tunerName);
}
else
{
mMainGui.setTitle(mTitle);
}
}
public class ShutdownMonitor extends WindowAdapter
{
@Override
public void windowClosing(WindowEvent e)
{
processShutdown();
preDestroy();
}
}
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
{
private ControllerPanel mControllerPanel;
public BroadcastStatusVisibleMenuItem(ControllerPanel controllerPanel)
{
super("Show Streaming Status");
mControllerPanel = controllerPanel;
setSelected(mBroadcastStatusPanel != null);
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
toggleBroadcastStatusPanelVisibility();
setSelected(mBroadcastStatusVisible);
}
});
}
}
public class TunersMenu extends JMenu
{
public TunersMenu()
{
super("Tuners");
addMenuListener(new MenuListener()
{
@Override
public void menuSelected(MenuEvent e)
{
removeAll();
for(DiscoveredTuner discoveredTuner: mTunerManager.getAvailableTuners())
{
add(new ShowTunerMenuItem(mTunerManager.getDiscoveredTunerModel(), discoveredTuner.getTuner()));
}
}
@Override
public void menuDeselected(MenuEvent e) { }
@Override
public void menuCanceled(MenuEvent e) { }
});
}
}
/**
* Launch the application.
*/
public static void main(String[] args)
{
new SDRTrunk();
boolean headless = GraphicsEnvironment.isHeadless();
//Set the user's preferred location for the database prior to starting the application
// String dbpath = new UserPreferences().getDirectoryPreference().getDirectoryApplicationRoot().toString();
String dbpath = new UserPreferences().getDirectoryPreference().getDirectoryDatabase().getParent().toString();
System.setProperty("derby.system.home", dbpath);
ConfigurableApplicationContext context = new SpringApplicationBuilder(SDRTrunk.class)
.bannerMode(Banner.Mode.OFF)
.headless(headless)
.web(WebApplicationType.NONE)
.registerShutdownHook(true)
.run(args);
}
}

View File

@@ -0,0 +1,709 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui;
import com.jidesoft.plaf.LookAndFeelFactory;
import com.jidesoft.swing.JideSplitPane;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.controller.ControllerPanel;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelAutoStartFrame;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.icon.ViewIconManagerRequest;
import io.github.dsheirer.gui.playlist.ViewPlaylistRequest;
import io.github.dsheirer.gui.preference.CalibrateRequest;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
import io.github.dsheirer.gui.preference.calibration.CalibrationDialog;
import io.github.dsheirer.gui.viewer.ViewRecordingViewerRequest;
import io.github.dsheirer.monitor.ResourceMonitor;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.tuner.Tuner;
import io.github.dsheirer.source.tuner.TunerEvent;
import io.github.dsheirer.source.tuner.manager.DiscoveredTuner;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerModel;
import io.github.dsheirer.source.tuner.ui.TunerSpectralDisplayManager;
import io.github.dsheirer.spectrum.DisableSpectrumWaterfallMenuItem;
import io.github.dsheirer.spectrum.ShowTunerMenuItem;
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.TimeStamp;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.Robot;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.prefs.Preferences;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.control.ButtonType;
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.imageio.ImageIO;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;
import javax.swing.plaf.metal.MetalLookAndFeel;
/**
* SDRTrunk primary user interface frame/window
*/
public class SDRTrunkUI extends JFrame implements Listener<TunerEvent>
{
private static final Logger mLog = LoggerFactory.getLogger(SDRTrunkUI.class);
private final Preferences mPreferences = Preferences.userNodeForPackage(SDRTrunkUI.class);
private static final String PREFERENCE_BROADCAST_STATUS_VISIBLE = "sdrtrunk.broadcast.status.visible";
private static final String PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE = "sdrtrunk.now.playing.details.visible";
private static final String PREFERENCE_RESOURCE_STATUS_VISIBLE = "sdrtrunk.resource.status.visible";
private static final String BASE_WINDOW_NAME = "sdrtrunk.main.window";
private static final String CONTROLLER_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".control.panel";
private static final String SPECTRAL_PANEL_IDENTIFIER = BASE_WINDOW_NAME + ".spectral.panel";
private static final String WINDOW_FRAME_IDENTIFIER = BASE_WINDOW_NAME + ".frame";
@Resource
private BroadcastStatusPanel mBroadcastStatusPanel;
@Resource
private ChannelModel mChannelModel;
@Resource
private ChannelProcessingManager mChannelProcessingManager;
@Resource
private ControllerPanel mControllerPanel;
@Resource
private DiscoveredTunerModel mDiscoveredTunerModel;
@Resource
private JavaFxWindowManager mJavaFxWindowManager;
@Resource
private ResourceMonitor mResourceMonitor;
@Resource
private SpectralDisplayPanel mSpectralPanel;
@Resource
private TunerSpectralDisplayManager mTunerSpectralDisplayManager;
@Resource
private TunerManager mTunerManager;
private JFXPanel mResourceStatusPanel;
private JideSplitPane mSplitPane;
private boolean mBroadcastStatusVisible;
private boolean mResourceStatusVisible;
private boolean mNowPlayingDetailsVisible;
@Resource
private UserPreferences mUserPreferences;
private String mTitle;
/**
* Constructs an instance of the SDRTrunk user interface
*/
public SDRTrunkUI()
{
//Register FontAwesome so we can use the fonts in Swing windows
IconFontSwing.register(FontAwesome.getIconFont());
}
/**
* Hook into this visible method so that we can show the auto-start channels dialog on window show.
*/
@Override
public void setVisible(boolean b)
{
super.setVisible(b);
CalibrationManager calibrationManager = CalibrationManager.getInstance(mUserPreferences);
final boolean calibrating = !calibrationManager.isCalibrated() &&
!mUserPreferences.getVectorCalibrationPreference().isHideCalibrationDialog();
if(calibrating)
{
Platform.runLater(() ->
{
CalibrationDialog calibrationDialog = mJavaFxWindowManager.getCalibrationDialog(mUserPreferences);
Optional<ButtonType> calibrate = calibrationDialog.showAndWait();
if(calibrate.isPresent() && calibrate.get().getText().equals("Calibrate"))
{
//Request focus and execute calibration
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.VECTOR_CALIBRATION));
MyEventBus.getGlobalEventBus().post(new CalibrateRequest());
}
else
{
autoStartChannels();
}
});
}
else
{
autoStartChannels();
}
}
/**
* Shows the auto-start channels dialog
*/
private void autoStartChannels()
{
List<Channel> channels = mChannelModel.getAutoStartChannels();
if(channels.size() > 0)
{
new ChannelAutoStartFrame(mChannelProcessingManager, channels, mUserPreferences);
}
}
@PostConstruct
public void postConstruct()
{
String operatingSystem = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
if(operatingSystem.contains("mac") || operatingSystem.contains("nux"))
{
try
{
UIManager.setLookAndFeel(MetalLookAndFeel.class.getName());
LookAndFeelFactory.installJideExtension();
}
catch(Exception e)
{
mLog.error("Error trying to set Metal look and feel for OS [" + operatingSystem + "]");
}
}
new ChannelSelectionManager();
mNowPlayingDetailsVisible = mPreferences.getBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, true);
mTunerSpectralDisplayManager.setSpectralDisplayPanel(mSpectralPanel);
mDiscoveredTunerModel.addListener(mTunerSpectralDisplayManager);
mDiscoveredTunerModel.addListener(this);
initGUI();
Tuner tuner = mTunerSpectralDisplayManager.showFirstTuner();
if(tuner != null)
{
updateTitle(tuner.getPreferredName());
}
}
/**
* Initialize the contents of the frame.
*/
private void initGUI()
{
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]0[shrink 0]"));
/**
* Setup main JFrame window
*/
mTitle = SystemProperties.getInstance().getApplicationName();
setTitle(mTitle);
Point location = mUserPreferences.getSwingPreference().getLocation(WINDOW_FRAME_IDENTIFIER);
if(location != null)
{
setLocation(location);
}
else
{
setLocationRelativeTo(null);
}
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
addWindowListener(new ShutdownMonitor());
Dimension dimension = mUserPreferences.getSwingPreference().getDimension(WINDOW_FRAME_IDENTIFIER);
mSpectralPanel.setPreferredSize(new Dimension(1280, 300));
mControllerPanel.setPreferredSize(new Dimension(1280, 500));
if(dimension != null)
{
Dimension spectral = mUserPreferences.getSwingPreference().getDimension(SPECTRAL_PANEL_IDENTIFIER);
if(spectral != null)
{
mSpectralPanel.setSize(spectral);
}
Dimension controller = mUserPreferences.getSwingPreference().getDimension(CONTROLLER_PANEL_IDENTIFIER);
if(controller != null)
{
mControllerPanel.setSize(controller);
}
setSize(dimension);
if(mUserPreferences.getSwingPreference().getMaximized(WINDOW_FRAME_IDENTIFIER, false))
{
setExtendedState(JFrame.MAXIMIZED_BOTH);
}
}
else
{
setSize(new Dimension(1280, 800));
}
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
mSplitPane.setDividerSize(5);
mSplitPane.add(mSpectralPanel);
mSplitPane.add(mControllerPanel);
mBroadcastStatusVisible = mPreferences.getBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, false);
//Show broadcast status panel when user requests - disabled by default
if(mBroadcastStatusVisible)
{
mSplitPane.add(getBroadcastStatusPanel());
}
add(mSplitPane, "cell 0 0,span,grow");
mResourceMonitor.start();
mResourceStatusVisible = mPreferences.getBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, true);
if(mResourceStatusVisible)
{
add(getResourceStatusPanel(), "span,growx");
}
/**
* Menu items
*/
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
JMenuItem exitMenu = new JMenuItem("Exit");
exitMenu.addActionListener(event -> {
shutdown();
System.exit(0);
}
);
fileMenu.add(exitMenu);
JMenu viewMenu = new JMenu("View");
JMenuItem viewPlaylistItem = new JMenuItem("Playlist Editor");
viewPlaylistItem.setIcon(IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 12));
viewPlaylistItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewPlaylistRequest()));
viewMenu.add(viewPlaylistItem);
viewMenu.add(new JSeparator());
JMenuItem viewApplicationLogsMenu = new JMenuItem("Application Log Files");
viewApplicationLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewApplicationLogsMenu.addActionListener(arg0 -> {
File logsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryApplicationLog().toFile();
try
{
Desktop.getDesktop().open(logsDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(this,
"Can't launch file explorer - files are located at: " + logsDirectory,
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(viewApplicationLogsMenu);
JMenuItem viewRecordingsMenuItem = new JMenuItem("Audio Recordings");
viewRecordingsMenuItem.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewRecordingsMenuItem.addActionListener(arg0 -> {
File recordingsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryRecording().toFile();
try
{
Desktop.getDesktop().open(recordingsDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(this,
"Can't launch file explorer - files are located at: " +
recordingsDirectory,
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(viewRecordingsMenuItem);
JMenuItem viewEventLogsMenu = new JMenuItem("Channel Event Log Files");
viewEventLogsMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewEventLogsMenu.addActionListener(arg0 -> {
File eventLogsDirectory = mUserPreferences.getDirectoryPreference().getDirectoryEventLog().toFile();
try
{
Desktop.getDesktop().open(eventLogsDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(this, "Can't launch file explorer - files are located at: " + eventLogsDirectory,
"Can't launch file explorer", JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(viewEventLogsMenu);
JMenuItem iconManagerMenu = new JMenuItem("Icon Manager");
iconManagerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.PICTURE_O, 12));
iconManagerMenu.addActionListener(arg0 -> MyEventBus.getGlobalEventBus().post(new ViewIconManagerRequest()));
viewMenu.add(iconManagerMenu);
JMenuItem recordingViewerMenu = new JMenuItem("Message Recording Viewer (.bits)");
recordingViewerMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.BRAILLE, 12));
recordingViewerMenu.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewRecordingViewerRequest()));
viewMenu.add(recordingViewerMenu);
JMenuItem viewScreenCapturesMenu = new JMenuItem("Screen Captures");
viewScreenCapturesMenu.setIcon(IconFontSwing.buildIcon(FontAwesome.FOLDER_OPEN_O, 12));
viewScreenCapturesMenu.addActionListener(arg0 -> {
File screenCapturesDirectory = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().toFile();
try
{
Desktop.getDesktop().open(screenCapturesDirectory);
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(this, "Can't launch file explorer - files are located at: " + screenCapturesDirectory,
"Can't launch file explorer", JOptionPane.ERROR_MESSAGE);
}
});
viewMenu.add(viewScreenCapturesMenu);
JMenuItem preferencesItem = new JMenuItem("User Preferences");
preferencesItem.setIcon(IconFontSwing.buildIcon(FontAwesome.COG, 12));
preferencesItem.addActionListener(e -> MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest()));
viewMenu.add(preferencesItem);
viewMenu.add(new JSeparator());
viewMenu.add(new TunersMenu());
viewMenu.add(new JSeparator());
viewMenu.add(new DisableSpectrumWaterfallMenuItem(mSpectralPanel, mUserPreferences));
viewMenu.add(new NowPlayingChannelDetailsVisibleMenuItem());
viewMenu.add(new BroadcastStatusVisibleMenuItem());
viewMenu.add(new ResourceStatusVisibleMenuItem());
menuBar.add(viewMenu);
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
screenCaptureItem.setIcon(IconFontSwing.buildIcon(FontAwesome.CAMERA, 12));
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
screenCaptureItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
screenCaptureItem.setMaximumSize(screenCaptureItem.getPreferredSize());
screenCaptureItem.addActionListener(arg0 -> {
try
{
Robot robot = new Robot();
final BufferedImage image = robot.createScreenCapture(getBounds());
String filename = TimeStamp.getTimeStamp("_") + "_screen_capture.png";
final Path captureFile = mUserPreferences.getDirectoryPreference().getDirectoryScreenCapture().resolve(filename);
EventQueue.invokeLater(() -> {
try
{
ImageIO.write(image, "png", captureFile.toFile());
}
catch(IOException e)
{
mLog.error("Couldn't write screen capture to file [" + captureFile.toString() + "]", e);
}
});
}
catch(AWTException e)
{
mLog.error("Exception while taking screen capture", e);
}
});
menuBar.add(screenCaptureItem);
}
/**
* Updates the title bar with the tuner name
* @param tunerName optional
*/
private void updateTitle(String tunerName)
{
if(tunerName != null)
{
setTitle(mTitle + " - " + tunerName);
}
else
{
setTitle(mTitle);
}
}
@Override
public void receive(TunerEvent event)
{
switch(event.getEvent())
{
case REQUEST_MAIN_SPECTRAL_DISPLAY:
updateTitle(event.getTuner().getPreferredName());
break;
case REQUEST_CLEAR_MAIN_SPECTRAL_DISPLAY:
updateTitle(null);
break;
case NOTIFICATION_SHUTTING_DOWN:
Tuner currentTuner = mSpectralPanel.getTuner();
if(event.hasTuner() && event.getTuner().equals(currentTuner) || currentTuner == null)
{
updateTitle(null);
}
break;
}
}
/**
* Lazy constructor for broadcast status panel
*/
private BroadcastStatusPanel getBroadcastStatusPanel()
{
return mBroadcastStatusPanel;
}
/**
* Toggles visibility of the broadcast channels status panel at the bottom of the controller panel
*/
private void toggleBroadcastStatusPanelVisibility()
{
mBroadcastStatusVisible = !mBroadcastStatusVisible;
EventQueue.invokeLater(() -> {
if(mBroadcastStatusVisible)
{
mSplitPane.add(getBroadcastStatusPanel());
}
else
{
mSplitPane.remove(getBroadcastStatusPanel());
}
revalidate();
});
mPreferences.putBoolean(PREFERENCE_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
}
/**
* Lazy constructor for resource status panel
*/
private JFXPanel getResourceStatusPanel()
{
if(mResourceStatusPanel == null)
{
mResourceStatusPanel = mJavaFxWindowManager.getStatusPanel(mResourceMonitor);
}
return mResourceStatusPanel;
}
/**
* Toggles visibility of the resource status panel at the bottom of the main UI window
*/
private void toggleResourceStatusPanelVisibility()
{
mResourceStatusVisible = !mResourceStatusVisible;
EventQueue.invokeLater(() -> {
if(mResourceStatusVisible)
{
add(getResourceStatusPanel(), "span,growx");
}
else
{
remove(getResourceStatusPanel());
}
revalidate();
});
mPreferences.putBoolean(PREFERENCE_RESOURCE_STATUS_VISIBLE, mResourceStatusVisible);
}
/**
* Toggles visibility of the Now Playing channel details panel
*/
private void toggleNowPlayingDetailsPanelVisibility()
{
mNowPlayingDetailsVisible = !mNowPlayingDetailsVisible;
mControllerPanel.getNowPlayingPanel().setDetailTabsVisible(mNowPlayingDetailsVisible);
mPreferences.putBoolean(PREFERENCE_NOW_PLAYING_DETAILS_VISIBLE, mNowPlayingDetailsVisible);
}
/**
* Performs shutdown operations. This is triggered by the shutdown monitor's windowClosing() method
*/
private void shutdown()
{
mLog.info("SDRTrunk UI shutdown started ...");
mUserPreferences.getSwingPreference().setLocation(WINDOW_FRAME_IDENTIFIER, getLocation());
mUserPreferences.getSwingPreference().setDimension(WINDOW_FRAME_IDENTIFIER, getSize());
mUserPreferences.getSwingPreference().setMaximized(WINDOW_FRAME_IDENTIFIER,
(getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH);
mUserPreferences.getSwingPreference().setDimension(SPECTRAL_PANEL_IDENTIFIER, mSpectralPanel.getSize());
mUserPreferences.getSwingPreference().setDimension(CONTROLLER_PANEL_IDENTIFIER, mControllerPanel.getSize());
mLog.info("Stopping JavaFX window manager ...");
mJavaFxWindowManager.shutdown();
mLog.info("Stopping resource monitor ...");
mResourceMonitor.stop();
mLog.info("Stopping spectral display ...");
mSpectralPanel.clearTuner();
}
/**
* Window adapter to receive window closing event to initiate shutdown.
*/
public class ShutdownMonitor extends WindowAdapter
{
@Override
public void windowClosing(WindowEvent e)
{
shutdown();
}
}
/**
* Broadcast status panel visible toggle menu item
*/
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
{
public BroadcastStatusVisibleMenuItem()
{
super("Show Streaming Status");
setSelected(mBroadcastStatusVisible);
addActionListener(e -> {
toggleBroadcastStatusPanelVisibility();
setSelected(mBroadcastStatusVisible);
});
}
}
/**
* Resource status panel visible toggle menu item
*/
public class ResourceStatusVisibleMenuItem extends JCheckBoxMenuItem
{
public ResourceStatusVisibleMenuItem()
{
super("Show Resource Status");
setSelected(mResourceStatusVisible);
addActionListener(e -> {
toggleResourceStatusPanelVisibility();
setSelected(mResourceStatusVisible);
});
}
}
/**
* Now Playing channel details visible toggle menu item
*/
public class NowPlayingChannelDetailsVisibleMenuItem extends JCheckBoxMenuItem
{
public NowPlayingChannelDetailsVisibleMenuItem()
{
super("Show Now Playing Channel Details");
setSelected(mNowPlayingDetailsVisible);
addActionListener(e -> {
toggleNowPlayingDetailsPanelVisibility();
setSelected(mNowPlayingDetailsVisible);
});
}
}
/**
* Menu for selecting tuners for display.
*/
public class TunersMenu extends JMenu
{
public TunersMenu()
{
super("Tuners");
addMenuListener(new MenuListener()
{
@Override
public void menuSelected(MenuEvent e)
{
removeAll();
for(DiscoveredTuner discoveredTuner: mTunerManager.getAvailableTuners())
{
add(new ShowTunerMenuItem(discoveredTuner.getTuner(), mDiscoveredTunerModel, mUserPreferences));
}
}
@Override
public void menuDeselected(MenuEvent e) { }
@Override
public void menuCanceled(MenuEvent e) { }
});
}
}
}

View File

@@ -0,0 +1,710 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzEditor;
import io.github.dsheirer.audio.call.CallView;
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelsView;
import io.github.dsheirer.gui.icon.IconManager;
import io.github.dsheirer.gui.playlist.PlaylistEditor;
import io.github.dsheirer.gui.playlist.alias.AliasBulkEditor;
import io.github.dsheirer.gui.playlist.alias.AliasConfigurationEditor;
import io.github.dsheirer.gui.playlist.alias.AliasEditor;
import io.github.dsheirer.gui.playlist.alias.AliasItemEditor;
import io.github.dsheirer.gui.playlist.alias.AliasViewByIdentifierEditor;
import io.github.dsheirer.gui.playlist.alias.AliasViewByRecordingEditor;
import io.github.dsheirer.gui.playlist.channel.AMConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.ChannelEditor;
import io.github.dsheirer.gui.playlist.channel.DMRConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.LTRConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.LTRNetConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.MPT1327ConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.NBFMConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.P25P1ConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.P25P2ConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.PassportConfigurationEditor;
import io.github.dsheirer.gui.playlist.channel.UnknownConfigurationEditor;
import io.github.dsheirer.gui.playlist.channelMap.ChannelMapEditor;
import io.github.dsheirer.gui.playlist.manager.PlaylistManagerEditor;
import io.github.dsheirer.gui.playlist.radioreference.CountyAgencyEditor;
import io.github.dsheirer.gui.playlist.radioreference.CountySystemEditor;
import io.github.dsheirer.gui.playlist.radioreference.NationalAgencyEditor;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceEditor;
import io.github.dsheirer.gui.playlist.radioreference.SiteEditor;
import io.github.dsheirer.gui.playlist.radioreference.StateAgencyEditor;
import io.github.dsheirer.gui.playlist.radioreference.StateSystemEditor;
import io.github.dsheirer.gui.playlist.radioreference.SystemSiteSelectionEditor;
import io.github.dsheirer.gui.playlist.radioreference.SystemTalkgroupSelectionEditor;
import io.github.dsheirer.gui.playlist.radioreference.TalkgroupEditor;
import io.github.dsheirer.gui.playlist.streaming.BroadcastifyCallEditor;
import io.github.dsheirer.gui.playlist.streaming.BroadcastifyStreamEditor;
import io.github.dsheirer.gui.playlist.streaming.IcecastHTTPStreamEditor;
import io.github.dsheirer.gui.playlist.streaming.IcecastTCPStreamEditor;
import io.github.dsheirer.gui.playlist.streaming.RdioScannerEditor;
import io.github.dsheirer.gui.playlist.streaming.ShoutcastV1StreamEditor;
import io.github.dsheirer.gui.playlist.streaming.ShoutcastV2StreamEditor;
import io.github.dsheirer.gui.playlist.streaming.StreamAliasSelectionEditor;
import io.github.dsheirer.gui.playlist.streaming.StreamingEditor;
import io.github.dsheirer.gui.playlist.streaming.UnknownStreamEditor;
import io.github.dsheirer.gui.preference.DecodeEventViewPreferenceEditor;
import io.github.dsheirer.gui.preference.TalkgroupFormatPreferenceEditor;
import io.github.dsheirer.gui.preference.UserPreferencesEditor;
import io.github.dsheirer.gui.preference.application.ApplicationPreferenceEditor;
import io.github.dsheirer.gui.preference.audio.call.CallManagementPreferenceEditor;
import io.github.dsheirer.gui.preference.audio.playback.PlaybackPreferenceEditor;
import io.github.dsheirer.gui.preference.audio.record.RecordPreferenceEditor;
import io.github.dsheirer.gui.preference.audio.retention.RetentionPreferenceEditor;
import io.github.dsheirer.gui.preference.calibration.VectorCalibrationPreferenceEditor;
import io.github.dsheirer.gui.preference.decoder.JmbeLibraryPreferenceEditor;
import io.github.dsheirer.gui.preference.directory.DirectoryPreferenceEditor;
import io.github.dsheirer.gui.preference.tuner.TunerPreferenceEditor;
import io.github.dsheirer.jmbe.JmbeEditor;
import java.awt.GraphicsEnvironment;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import jiconfont.javafx.IconFontFX;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
/**
* Spring configuration factory for JavaFX UI components that require dependency injection (@Resource)
*
* Note: all bean creator methods are also annotated as @Lazy so that Spring doesn't auto-instantiate these beans when
* we're running in a headless environment.
*/
@Configuration
public class SpringJavaFxBeanFactory
{
private JFXPanel mJFXPanel;
/**
* Constructs an instance
*/
public SpringJavaFxBeanFactory()
{
if(!GraphicsEnvironment.isHeadless())
{
//Initialize the JavafX platform by creating a JFXPanel.
createJFXPanel();
//Register JavaFX icon fonts
IconFontFX.register(jiconfont.icons.font_awesome.FontAwesome.getIconFont());
IconFontFX.register(jiconfont.icons.elusive.Elusive.getIconFont());
}
}
/**
* Initialize the JavaFX platform by creating a JavaFX panel for Swing application compatibility
*/
private void createJFXPanel()
{
if(mJFXPanel == null)
{
mJFXPanel = new JFXPanel();
Platform.setImplicitExit(false);
}
}
/**
* Call view Panel
*/
@Bean
@Lazy
public CallView getCallViewPanel()
{
return new CallView();
}
/**
* Audio playback channels view
*/
@Bean
@Lazy
public AudioPlaybackChannelsView getAudioPlaybackChannelsView()
{
return new AudioPlaybackChannelsView();
}
/**
* Alias Item Editor
*/
@Bean
@Lazy
public AliasItemEditor getAliasItemEditor()
{
return new AliasItemEditor();
}
/**
* Alias Bulk Editor
*/
@Bean
@Lazy
public AliasBulkEditor getAliasBulkEditor()
{
return new AliasBulkEditor();
}
/**
* Alias Configuration Editor
*/
@Bean
@Lazy
public AliasConfigurationEditor getAliasConfigurationEditor()
{
return new AliasConfigurationEditor();
}
/**
* Alias Editor
*/
@Bean
@Lazy
public AliasEditor getAliasEditor()
{
return new AliasEditor();
}
/**
* Alias View By Identifier Editor
*/
@Bean
@Lazy
public AliasViewByIdentifierEditor getAliasViewByIdentifierEditor()
{
return new AliasViewByIdentifierEditor();
}
/**
* Channel Editor
*/
@Bean
@Lazy
public ChannelEditor getChannelEditor()
{
return new ChannelEditor();
}
/**
* Playlist Editor
*/
@Bean
@Lazy
public PlaylistEditor getPlaylistEditor()
{
return new PlaylistEditor();
}
/**
* Playlist Manager Editor
*/
@Bean
@Lazy
public PlaylistManagerEditor getPlaylistManagerEditor()
{
return new PlaylistManagerEditor();
}
/**
* Radio Reference Editor
*/
@Bean
@Lazy
public RadioReferenceEditor getRadioReferenceEditor()
{
return new RadioReferenceEditor();
}
/**
* Stream alias selection editor
*/
@Bean
@Lazy
public StreamAliasSelectionEditor getStreamAliasSelectionEditor()
{
return new StreamAliasSelectionEditor();
}
/**
* Streaming editor
*/
@Bean
@Lazy
public StreamingEditor getStreamingEditor()
{
return new StreamingEditor();
}
/**
* AM channel configuration editor
*/
@Bean
@Lazy
public AMConfigurationEditor getAMConfigurationEditor()
{
return new AMConfigurationEditor();
}
/**
* NBFM channel configuration editor
*/
@Bean
@Lazy
public NBFMConfigurationEditor getNBFMConfigurationEditor()
{
return new NBFMConfigurationEditor();
}
/**
* DMR channel configuration editor
*/
@Bean
@Lazy
public DMRConfigurationEditor getDMRConfigurationEditor()
{
return new DMRConfigurationEditor();
}
/**
* LTR channel configuration editor
*/
@Bean
@Lazy
public LTRConfigurationEditor getLTRConfigurationEditor()
{
return new LTRConfigurationEditor();
}
/**
* LTR-Net channel configuration editor
*/
@Bean
@Lazy
public LTRNetConfigurationEditor getLTRNetConfigurationEditor()
{
return new LTRNetConfigurationEditor();
}
/**
* MPT-1327 channel configuration editor
*/
@Bean
@Lazy
public MPT1327ConfigurationEditor getMPT1327ConfigurationEditor()
{
return new MPT1327ConfigurationEditor();
}
/**
* P25 Phase 1 channel configuration editor
*/
@Bean
@Lazy
public P25P1ConfigurationEditor getP25P1ConfigurationEditor()
{
return new P25P1ConfigurationEditor();
}
/**
* P25 Phase 2 channel configuration editor
*/
@Bean
@Lazy
public P25P2ConfigurationEditor getP25P2ConfigurationEditor()
{
return new P25P2ConfigurationEditor();
}
/**
* Passport channel configuration editor
*/
@Bean
@Lazy
public PassportConfigurationEditor getPassportConfigurationEditor()
{
return new PassportConfigurationEditor();
}
/**
* Unknown channel configuration editor
*/
@Bean
@Lazy
public UnknownConfigurationEditor getUnknownConfigurationEditor()
{
return new UnknownConfigurationEditor();
}
/**
* Broadcastify stream editor
*/
@Bean
@Lazy
public BroadcastifyStreamEditor getBroadcastifyStreamEditor()
{
return new BroadcastifyStreamEditor();
}
/**
* Broadcastify calls stream editor
*/
@Bean
@Lazy
public BroadcastifyCallEditor getBroadcastifyCallEditor()
{
return new BroadcastifyCallEditor();
}
/**
* OpenMHZ stream editor
*/
@Bean
@Lazy
public OpenMHzEditor getOpenMhzEditor()
{
return new OpenMHzEditor();
}
/**
* Rdio scanner stream editor
*/
@Bean
@Lazy
public RdioScannerEditor getRdioScannerEditor()
{
return new RdioScannerEditor();
}
/**
* Icecast TCP stream editor
*/
@Bean
@Lazy
public IcecastTCPStreamEditor getIcecastTcpStreamEditor()
{
return new IcecastTCPStreamEditor();
}
/**
* Icecast HTTP stream editor
*/
@Bean
@Lazy
public IcecastHTTPStreamEditor getIcecastHTTPStreamEditor()
{
return new IcecastHTTPStreamEditor();
}
/**
* Shoutcast V1 stream editor
*/
@Bean
@Lazy
public ShoutcastV1StreamEditor getShoutcastV1StreamEditor()
{
return new ShoutcastV1StreamEditor();
}
/**
* Shoutcast V1 stream editor
*/
@Bean
@Lazy
public ShoutcastV2StreamEditor getShoutcastV2StreamEditor()
{
return new ShoutcastV2StreamEditor();
}
/**
* Unknown stream editor
*/
@Bean
@Lazy
public UnknownStreamEditor getUnknownStreamEditor()
{
return new UnknownStreamEditor();
}
/**
* County agency editor
*/
@Bean
@Lazy
public CountyAgencyEditor getCountyAgencyEditor()
{
return new CountyAgencyEditor();
}
/**
* County system editor
*/
@Bean
@Lazy
public CountySystemEditor getCountySystemEditor()
{
return new CountySystemEditor();
}
/**
* State agency editor
*/
@Bean
@Lazy
public StateAgencyEditor getStateAgencyEditor()
{
return new StateAgencyEditor();
}
/**
* State system editor
*/
@Bean
@Lazy
public StateSystemEditor getStateSystemEditor()
{
return new StateSystemEditor();
}
/**
* National agency editor
*/
@Bean
@Lazy
public NationalAgencyEditor getNationalAgencyEditor()
{
return new NationalAgencyEditor();
}
/**
* System talkgroup selection editor
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public SystemTalkgroupSelectionEditor getSystemTalkgroupSelectionEditor()
{
return new SystemTalkgroupSelectionEditor();
}
/**
* Site editor
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public SiteEditor getSiteEditor()
{
return new SiteEditor();
}
/**
* Talkgroup editor
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public TalkgroupEditor getTalkgroupEditor()
{
return new TalkgroupEditor();
}
/**
* System site selection editor
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public SystemSiteSelectionEditor getSystemSiteSelectionEditor()
{
return new SystemSiteSelectionEditor();
}
/**
* Channel map editor
*/
@Bean
@Lazy
public ChannelMapEditor getChannelMapEditor()
{
return new ChannelMapEditor();
}
/**
* Channel map editor
*/
@Bean
@Lazy
public IconManager getIconManager()
{
return new IconManager();
}
/**
* User preferences editor
*/
@Bean
@Lazy
public UserPreferencesEditor getUserPreferencesEditor()
{
return new UserPreferencesEditor();
}
/**
* Jmbe editor
*/
@Bean
@Lazy
public JmbeEditor getJmbeEditor()
{
return new JmbeEditor();
}
/**
* Alias view-by recording editor
*/
@Bean
@Lazy
public AliasViewByRecordingEditor getAliasViewByRecordingEditor()
{
return new AliasViewByRecordingEditor();
}
/**
* Application preference editor
*/
@Bean
@Lazy
public ApplicationPreferenceEditor getApplicationPreferenceEditor()
{
return new ApplicationPreferenceEditor();
}
/**
* Call management preference editor
*/
@Bean
@Lazy
public CallManagementPreferenceEditor getCallManagementPreferenceEditor()
{
return new CallManagementPreferenceEditor();
}
/**
* Playback preference editor
*/
@Bean
@Lazy
public PlaybackPreferenceEditor getPlaybackPreferenceEditor()
{
return new PlaybackPreferenceEditor();
}
/**
* Record preference editor
*/
@Bean
@Lazy
public RecordPreferenceEditor getRecordPreferenceEditor()
{
return new RecordPreferenceEditor();
}
/**
* Audio/call retention preference editor
*/
@Bean
@Lazy
public RetentionPreferenceEditor getRetentionPreferenceEditor()
{
return new RetentionPreferenceEditor();
}
/**
* Decode event view preference editor
*/
@Bean
@Lazy
public DecodeEventViewPreferenceEditor getDecodeEventViewPreferenceEditor()
{
return new DecodeEventViewPreferenceEditor();
}
/**
* Directory preference editor
*/
@Bean
@Lazy
public DirectoryPreferenceEditor getDirectoryPreferenceEditor()
{
return new DirectoryPreferenceEditor();
}
/**
* Playback preference editor
*/
@Bean
@Lazy
public JmbeLibraryPreferenceEditor getJmbeLibraryPreferenceEditor()
{
return new JmbeLibraryPreferenceEditor();
}
/**
* Tuner preference editor
*/
@Bean
@Lazy
public TunerPreferenceEditor getTunerPreferenceEditor()
{
return new TunerPreferenceEditor();
}
/**
* Talkgroup format preference editor
*/
@Bean
@Lazy
public TalkgroupFormatPreferenceEditor getTalkgroupFormatPreferenceEditor()
{
return new TalkgroupFormatPreferenceEditor();
}
/**
* Vector calibration preference editor
*/
@Bean
@Lazy
public VectorCalibrationPreferenceEditor getVectorCalibrationPreferenceEditor()
{
return new VectorCalibrationPreferenceEditor();
}
}

View File

@@ -0,0 +1,249 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.audio.playback.AudioPanel;
import io.github.dsheirer.channel.details.ChannelDetailPanel;
import io.github.dsheirer.channel.metadata.ChannelMetadataPanel;
import io.github.dsheirer.channel.metadata.NowPlayingPanel;
import io.github.dsheirer.controller.ControllerPanel;
import io.github.dsheirer.gui.power.ChannelPowerPanel;
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
import io.github.dsheirer.source.tuner.ui.DiscoveredTunerEditor;
import io.github.dsheirer.source.tuner.ui.TunerViewPanel;
import io.github.dsheirer.spectrum.FrequencyOverlayPanel;
import io.github.dsheirer.spectrum.OverlayPanel;
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.spectrum.SpectrumFrame;
import io.github.dsheirer.spectrum.SpectrumPanel;
import io.github.dsheirer.spectrum.WaterfallPanel;
import java.awt.GraphicsEnvironment;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
/**
* Spring configuration factory for Swing UI components that require dependency injection (@Resource)
*
* Note: all bean creator methods are also annotated as @Lazy so that Spring doesn't auto-instantiate these beans when
* we're running in a headless environment.
*/
@Configuration
public class SpringSwingBeanFactory
{
/**
* Constructs an instance
*/
public SpringSwingBeanFactory()
{
}
/**
* Constructs the primary UI instance when running in non-headless mode.
*/
@Bean
@Lazy
public SDRTrunkUI getSDRTrunkUI()
{
if(GraphicsEnvironment.isHeadless())
{
return null;
}
return new SDRTrunkUI();
}
/**
* Constructs a new spectrum frame
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public SpectrumFrame createSpectrumFrame()
{
return new SpectrumFrame();
}
/**
* Constructs the spectral display panel
*/
@Bean
@Lazy
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public SpectralDisplayPanel getSpectralDisplayPanel()
{
return new SpectralDisplayPanel();
}
/**
* Constructs the spectrum panel
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public SpectrumPanel getSpectrumPanel()
{
return new SpectrumPanel();
}
/**
* Constructs the waterfall panel
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public WaterfallPanel getWaterfallPanel()
{
return new WaterfallPanel();
}
/**
* Constructs the overlay panel
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public OverlayPanel getOverlayPanel()
{
return new OverlayPanel();
}
/**
* Constructs the frequency overlay panel
*
* Note: editor is used in multiple places so we create a new instance for each (ie scope=prototype)
*/
@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Lazy
public FrequencyOverlayPanel getFrequencyOverlayPanel()
{
return new FrequencyOverlayPanel();
}
/**
* Creates a broadcast (ie streaming) status panel
*/
@Bean
@Lazy
public BroadcastStatusPanel getBroadcastStatusPanel()
{
return new BroadcastStatusPanel();
}
/**
* Creates the controller panel
*/
@Bean
@Lazy
public ControllerPanel controllerPanel()
{
return new ControllerPanel();
}
/**
* Creates the now playing panel
*/
@Bean
@Lazy
public NowPlayingPanel getNowPlayingPanel()
{
return new NowPlayingPanel();
}
/**
* Creates the channel metadata panel
*/
@Bean
@Lazy
public ChannelMetadataPanel getChannelMetadataPanel()
{
return new ChannelMetadataPanel();
}
/**
* Creates the channel detail panel
*/
@Bean
@Lazy
public ChannelDetailPanel getChannelDetailPanel()
{
return new ChannelDetailPanel();
}
/**
* Creates the channel power panel
*/
@Bean
@Lazy
public ChannelPowerPanel getChannelPowerPanel()
{
return new ChannelPowerPanel();
}
/**
* Creates the decode event panel
*/
@Bean
@Lazy
public DecodeEventPanel getDecodeEventPanel()
{
return new DecodeEventPanel();
}
/**
* Creates an audio panel
*/
@Bean
@Lazy
public AudioPanel getAudioPanel()
{
return new AudioPanel();
}
/**
* Creates discovered tuner editor
*/
@Bean
@Lazy
public DiscoveredTunerEditor getDiscoveredTunerEditor()
{
return new DiscoveredTunerEditor();
}
/**
* Creates tuner view panel
*/
@Bean
@Lazy
public TunerViewPanel getTunerViewPanel()
{
return new TunerViewPanel();
}
}

View File

@@ -0,0 +1,30 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.calls;
import javafx.scene.layout.VBox;
/**
* Call audio panel for managing call audio playback.
*/
public class CallAudioPanel extends VBox
{
}

Some files were not shown because too many files have changed in this diff Show More