Compare commits

..

173 Commits

Author SHA1 Message Date
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
Denny Sheirer
02363c3113 Merge pull request #1690 from DSheirer/1689-harris-patch-group-prefixes
#1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris Patch Groups
2023-11-04 07:13:10 -04:00
Dennis Sheirer
cfaac0fcbc #1689 Increment Build 0.6.0 Beta 3 and Remove TG/RA prefixes for Harris patch groups. 2023-11-04 07:12:29 -04:00
Denny Sheirer
bee0405e00 Merge pull request #1688 from DSheirer/1687-rtl2832-fc0013
Support For RTL2832 With Embedded FC0013 Tuner
2023-11-04 06:54:55 -04:00
Dennis Sheirer
50bdf0ee81 #1687 Adds support for RTL2832 with embedded FC0013 tuner. 2023-11-04 06:52:37 -04:00
Denny Sheirer
a27ddbe0d0 Merge pull request #1679 from DSheirer/1643-r828d-v4-tuner
#1643 RTL2832/R828D aka RTL-SDR V4 Dongle & Bias-T Support
2023-10-22 08:40:33 -04:00
Dennis Sheirer
0d652b9153 #1643 Adds support for RTL2832 with embedded R828D tuner and RTL-SDR.com V4 dongles. Introduces Bias-T support for all RTL-2832 dongles. 2023-10-22 08:38:16 -04:00
Denny Sheirer
9468fd189b Merge pull request #1677 from DSheirer/1607-harris-p25-patch-groups
#1607 P25 P1/P2 L3Harris Patch Group Support
2023-10-21 07:00:41 -04:00
sheirerd
215737ad9b #1607 Add support for L3Harris dynamic regroup/patch messages and events. Includes parsing for L3Harris unknown opcodes 10, 42 and 43. 2023-10-21 06:58:11 -04:00
Denny Sheirer
123c25b142 Merge pull request #1672 from DSheirer/1650-fm-decoder-7_5-bandwidth
#1650 Adds 7.5 kHz channel bandwidth option for FM channels.
2023-10-15 09:22:48 -04:00
Dennis Sheirer
af56198f0e #1650 Adds 7.5 kHz channel bandwidth option for FM channels. 2023-10-15 09:21:38 -04:00
Denny Sheirer
3c73fdfd6e Merge pull request #1671 from DSheirer/1670-dmr-cap-plus-missing-call-audio
#1670 Missing Audio Segments - DMR Cap+
2023-10-15 08:56:48 -04:00
Dennis Sheirer
b391ebbb5f #1670 Resolves missing audio segments for some DMR Cap+ call events. 2023-10-15 08:55:35 -04:00
Denny Sheirer
bad5a1f9f0 Merge pull request #1669 from DSheirer/1668-message-viewer-p25-support
#1668 Message Recording Viewer - P25P1 Support
2023-10-15 07:17:06 -04:00
Dennis Sheirer
a7470b463f #1668 Updates message recording (.bits) viewer to support P25 Phase 1. 2023-10-15 07:14:53 -04:00
Denny Sheirer
d6c706cb35 Merge pull request #1667 from DSheirer/1666-p25-moto-unit-gps
#1666 P25 Motorola Unit GPS Parsing & Plot to Map
2023-10-14 08:16:48 -04:00
Dennis Sheirer
a0d6950c75 #1666 P25 Motorola unit self-reported GPS location parsing and plotting to map. 2023-10-14 08:15:35 -04:00
Denny Sheirer
2495200b36 Merge pull request #1663 from DSheirer/1648-dmr-lrrp-not-plotting-map
#1648 DMR Embedded GPS & LRRP Plotting
2023-10-05 03:32:08 -04:00
Dennis Sheirer
bfe7ae0b1b #1648 Resolves issue with plotting DMR embedded GPS and LRRP positions. 2023-10-05 03:31:17 -04:00
Denny Sheirer
4912abbf07 Merge pull request #1662 from DSheirer/1661-dmr-gps-location-parsing-error
#1661 DMR Talker GPS Lat/Long Values Incorrect
2023-10-05 03:21:24 -04:00
Dennis Sheirer
2f2998d02e #1661 Resolves issue with parsing of DMR embedded GPS latitude and longitude values. 2023-10-05 03:20:17 -04:00
Dennis Sheirer
c96349fa1c Updates version to 0.6.0 Beta 2 2023-08-31 07:14:58 -04:00
Denny Sheirer
f640576cc3 Merge pull request #1646 from DSheirer/1637-dmr-cap-plus-and-max-trunking
#1637 DMR Cap+/CapMax Channel Frequency Map / Trunking Issue
2023-08-31 07:10:21 -04:00
Dennis Sheirer
77d725a488 #1637 Resolves issue with DMR Cap+/CapMax trunking channel to frequency map. Updates the DMR channel map to use LCN instead of LSN and aligns standard DMR with Tier-III DMR for channel identification.
Warning: breaking change.  Users will need to either recreate their DMR channel to frequency maps or simply delete and reload the channel from Radio Reference to correct the channel to frequency map.

Updates the DMR Decoder State to better track and update DMR events versus creating a new event and causing lots of scrolling in the event window of the Now Playing tab.
2023-08-31 07:06:50 -04:00
Denny Sheirer
2c0305f928 Merge pull request #1642 from DSheirer/1641-hytera-rrs-packets
#1641 DMR Hytera RRS packet support.
2023-08-27 07:47:14 -04:00
Dennis Sheirer
21fd61d507 #1641 DMR Hytera RRS packet support. 2023-08-27 07:44:43 -04:00
Denny Sheirer
8fb56a5342 Merge pull request #1640 from DSheirer/1638-hytera-encryption-parameters
#1638 DMR Decoder - Encryption Parameters & Hytera RRS
2023-08-26 07:36:31 -04:00
Dennis Sheirer
5d7b73f876 #1638 Enhances DMR decoder to include additional encryption details from two major vendor implementations. Adds Hytera short data Radio Registration Service (RRS) processing. 2023-08-26 07:32:46 -04:00
Dennis Sheirer
85b2a38058 Version 0.6.0 Beta 1 2023-08-19 05:16:20 -04:00
Denny Sheirer
c78517ee75 Merge pull request #1631 from DSheirer/1630-flc-arc4-encryption-parameters
#1630 DMR FLC ARC4/EP Encryption Parameters
2023-08-18 07:08:30 -04:00
Dennis Sheirer
ae116d3dfe #1630 Updates ARC4 FLC encryption parameters message to detect and parse both Hytera & Motorola message formats. 2023-08-18 07:06:26 -04:00
Denny Sheirer
93252f9a40 Merge pull request #1629 from DSheirer/1628-libusb-device-list-error
#1628 LibUsb Error -99 During Device Discovery
2023-08-18 03:57:50 -04:00
Dennis Sheirer
3c03a4db2e #1628 Handle libusb get device list error condition and retry with increased logging during error code -99 to determine what causes this error condition, primarily on some Linux systems. 2023-08-18 03:55:42 -04:00
Denny Sheirer
d0fcd617aa Merge pull request #1627 from DSheirer/1626-dmr-call-events-missing-timeslot
#1626 Updates DMR call events to include timeslot and adds DMR encryption info when available.
2023-08-15 04:01:52 -04:00
Dennis Sheirer
6ca0decbd6 #1626 Updates DMR call events to include timeslot and adds DMR encryption information when available. 2023-08-15 04:01:27 -04:00
Denny Sheirer
ddd9ca933e Merge pull request #1624 from DSheirer/1623-unknown-opcode-message-filter
#1623 P25 Message Filter Set - Unknown Vendor Opcode
2023-08-14 04:04:30 -04:00
Dennis Sheirer
b2cc29569e #1623 resolves issue with handling of unknown P25 trunking opcode in message filter set. 2023-08-14 04:03:06 -04:00
Denny Sheirer
411735772b Merge pull request #1622 from DSheirer/dmr-cap-plus-identifier-updates
#1618 DMR Decoder Message Enhancements
2023-08-13 08:38:12 -04:00
sheirerd
2ffb2963af #1618 Update Cap+ radio & group parsing, process TXI & voice frame encryption parameters, and many other changes to the DMR message processing. 2023-08-13 08:30:23 -04:00
Denny Sheirer
227c88c8a2 Merge pull request #1614 from DSheirer/1612-dcs-alias-identifier
#1612 Enhance aliasing to support DCS tones
2023-07-24 04:34:46 -04:00
Dennis Sheirer
cbe9c79498 #1612 Enhance aliasing to support DCS tones 2023-07-24 04:30:16 -04:00
Denny Sheirer
1b8c100b43 Merge pull request #1611 from DSheirer/1605-tuner-editor-fails-to-display
#1605 Blank Tuner Editor Panel on Certain Linux Configurations
2023-07-22 06:00:38 -04:00
Dennis Sheirer
30c5fd64f2 #1605 Resolves issue where certain Linux users were unable to display the tuner editor panel due to an issue with the frequency control editor's usage of custom cursor. 2023-07-22 05:55:43 -04:00
Denny Sheirer
3a6b486340 Merge pull request #1610 from DSheirer/1604-event-panel-row-layout
#1604 Event Panel UI Layout Creates Excess Space On Resizing
2023-07-22 05:25:11 -04:00
Dennis Sheirer
ded7335dbf #1604 Resolves issue with event panel UI layout creating excess space between history management panel and events table. 2023-07-22 05:21:26 -04:00
Denny Sheirer
005a4f8119 Merge pull request #1608 from DSheirer/1603-duplicate-events
#1603 De-Duplicate Call Events
2023-07-22 05:06:11 -04:00
Dennis Sheirer
1ac49bfa7f #1603 Resolves issue with de-duplicating events to allow call duration values to display correctly and minimize the quantity of duplicate events. 2023-07-22 04:57:26 -04:00
Denny Sheirer
c302c4d121 Merge pull request #1601 from DSheirer/1368-decode-event-filtering-2
1368 Enhance Message & Event Filtering Options
2023-07-14 06:51:57 -04:00
Dennis Sheirer
b0a524cf62 #1368 Restructures Message and Event filter framework. 2023-07-14 06:50:07 -04:00
Christopher Varner
d246ffff30 Adds event type as a filtering option in the Events tab 2023-07-03 05:56:50 -04:00
Denny Sheirer
adfc8b2d6f #1586 Resolves issue in P25 message framer where after transmitting the current message the mDataUnitId is set to null and this causes an NPE when subsequent code is executed prior to shutdown of the channel. (#1593)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-07-03 05:11:20 -04:00
Denny Sheirer
ad2ab08466 #1588 Use JDK20 and Gradle 8.2. Updates SDRPlay API for compatibility with JDK20 Project Panama Foreign Memory/Function changes. (#1589)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-07-02 06:42:27 -04:00
Denny Sheirer
f3369410b2 #1590 Resolves issue with RSP tuners generating too many native buffers causing excessive memory consumption (#1591)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-07-02 06:23:00 -04:00
Denny Sheirer
bac38a5bd8 #1584 Updates DMR & P25 decoders to stop processing residual samples from the current buffer after a traffic channel shutdown has been signalled. (#1585)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-06-25 03:56:08 -04:00
Denny Sheirer
e5f5df74ad #1563 Updates RSP tuner to produce sample buffers with optimal power-of-2 length to enable downstream vectorized operations. (#1583)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-06-24 09:09:30 -04:00
Denny Sheirer
b3837f581a #1581 Updates Rdio Scanner configuration editor to automatically append the 'api/call-upload' path to the end of the protocol and host value provided by the user. Sets the host default value to 'http://localhost' but allows the user to overwrite. (#1582)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-06-24 07:05:44 -04:00
Denny Sheirer
b2ab6f58e9 #1579 Resolves issue with array index out of bounds error generated in P25 Phase 1 decoders where the Dibit circular buffer attempts to write a (#1580)
dibit past the length of the array.  Issue likely occurs when a partial/incomplete put operation adds a dibit and increments the buffer pointer but the thread is interrupted prior to wrapping the buffer pointer.  When the next runnable iteration executes, the pointer is pointing past the end of the buffer.  Updates the code to check the pointer both before and after the put operation.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-06-24 06:09:57 -04:00
Denny Sheirer
e04b59ba15 #1575 Updates traffic channel names to be 'T-' concatenated with the parent control channel name from the channel configuration. (#1578)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-06-24 05:15:27 -04:00
Denny Sheirer
366eacd234 #1573 Adds Digital Coded Squelch (DCS) auxiliary decoder support. (#1574)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-06-19 05:13:58 -04:00
Denny Sheirer
f792876c0a #1568 Updates NBFM decoder configuration so user can enable/disable high-pass filter option in the audio module to effect audio playback and audio recording of the demodulated audio. Disabling the filter allows recording of DC and sub-audible signalling that may be present in the demodulated audio (e.g. LTR or squelching signalling). (#1569)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2023-06-05 07:19:16 -04:00
Denny Sheirer
19f7a5171b #1565 updates LTR, LTRNet and Passport decoders with new slope decoder. Resolves issue with LTR Standard decoder state that was combining individual calls to the same talkgroup and causing excessively long calls. (#1567)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-06-05 06:13:12 -04:00
Denny Sheirer
5b6377cc11 #1558 Resolves issue where heterodyne channelizer allows tuning a set of channels that exceed the tuner's bandwidth. (#1561)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-21 06:12:11 -04:00
Denny Sheirer
9502ca3069 #1547 Updates playlist alias editor to make add/delete operations more efficient. (#1560)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-21 04:54:26 -04:00
Denny Sheirer
77629000b2 #1548 Reduces default decoder AF gain by 40% to resolve issue with distorted audio from local strong aircraft transmissions. (#1559)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-21 04:16:51 -04:00
Nokoa
ca7916f992 added Rsp1TunerConfiguration to JsonSubTypes of RspTunerConfiguration (#1555)
Following reports that RSP1 tuner configurations are reset to default on startup. Unable to test
2023-05-20 07:45:05 -04:00
Denny Sheirer
9de83843a3 #1556 DMR Capacity Plus parsing enhancements. (#1557)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-20 07:44:07 -04:00
tadscottsmith
cbab7bb0c9 Adds native Rdio Scanner streaming. (#1) (#1476)
#1476 Adds rdio-scanner call upload API streaming support
---------

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-15 05:28:07 -04:00
wedgef5
3df8d60eed #1507 Resolves issue with Cap+ LSN parsing. Updates Cap+ Site Status and adds RAS bit handling. (#1536)
Co-authored-by: Matthew Foster <matthew.foster@noaa.gov>
2023-05-14 08:03:49 -04:00
Denny Sheirer
b39dd1a19f #648 Resolves issue with audio panel sometimes displaying stale metadata that doesn't clear until the next audio segment is loaded. (#1545)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-10 04:29:46 -04:00
Denny Sheirer
0af5963d08 #1528 Restores consistent audio segment start tone insertion, broken in #1506 (#1543)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-08 03:59:42 -04:00
Denny Sheirer
4f8b8e978b #35 Updates AM & NBFM decoders to use Adaptive Squelch, a power squelch that includes an adaptive noise floor monitor. Implements vectorized magnitude operations for AM & FM decoders. Adds talkgroup assignment and squelch controls to AM channel configuration editor. Implements new AM gain control that attempts to normalize audio gain quickly at beginning of call/transmission and maintains that gain across lifecycle of the call and resets the gain for the next call. (#1540)
Notes on usage:
- When using either AM or FM squelch control use either the Heterodyne channelizer or if using the Polyphase channelizer, avoid using trunk tracking decoders like DMR or P25 or set the squelch threshold high enough to avoid changes in noise floor from polyphase channelizer where it can vary between using the one channel or the two-channel combined channel sources.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-05-07 07:27:44 -04:00
Dennis Sheirer
5654254cbb Revert "#1531 Updates version to 0.6.0 alpha 7 (#1532)"
This reverts commit 11f37e896a.
2023-04-24 04:39:20 -04:00
Denny Sheirer
11f37e896a #1531 Updates version to 0.6.0 alpha 7 (#1532)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-24 04:38:27 -04:00
Denny Sheirer
e8f563deeb #1529 Adds logging suppression to Broadcaster to limit total logged error messages. (#1530)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-24 04:36:02 -04:00
Denny Sheirer
f877354a11 #1455 Add Airspy HF+/Discovery Tuner Support (#1524)
* #1455 Add support for airspy hf+/discovery tuners

* #1455 testing updates

---------

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-15 07:18:37 -04:00
Denny Sheirer
df3d3b5ddc #1519 Adds null checking to channel results array to detect null on tuner shutdown. (#1520)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-13 04:31:46 -04:00
Denny Sheirer
59e74d70b4 #1515 Updates version to 0.6.0 alpha 5 (#1516)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-12 03:14:54 -04:00
Denny Sheirer
8d6b7c3ac8 #1511 Resolves issue with SDRPlay library crashes application when linux service is in stopped state. (#1514)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-11 04:32:58 -04:00
Denny Sheirer
372d2ff870 #1506 Resolves multiple performance issues: tuner freezing after a period of run time, buffer overflows, degraded audio playback. (#1513)
-Reduces message and event histories per channel from 500 to 200.
-Resolves issue of tuner freezing due to cancelled USB transfer buffers not being resubmitted.
-Reworks the audio playback system to remove in-progress call preemption and remove thread locks which can cause glitchy behavior during audio playback.
-Updates audio outputs to also use single thread pool for processing audio segments.
-Updates MBECallSequenceConverter to batch convert MBE to WAV audio.
-Unifies recording filename prefixes across baseband, mbe and binary recordings.
-Resolves issue with registering heartbeat manager to dispatcher.
-Uses single-thread thread pools for Dispatcher to support batch & process to overcome context switching consistency issue in Windows.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-04-11 04:07:41 -04:00
Denny Sheirer
32a8363b45 Update issue templates
Updated issue templates.
2023-03-25 06:49:58 -04:00
Denny Sheirer
87f1f8986f Update issue templates
Adds issue template to prevent end-users from posting support questions in the issues/bug tracker.
2023-03-25 06:48:08 -04:00
Denny Sheirer
b3c08053fb Update issue templates
Adds feature request template.
2023-03-25 06:42:27 -04:00
Denny Sheirer
a5208dcf1e Update issue templates
Initial bug report template.
2023-03-25 06:40:11 -04:00
Denny Sheirer
2782953518 #1500 Makes Channel Auto-Start timeout a user-preference item to select in range 0-30 seconds. (#1501)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-21 04:32:28 -04:00
Denny Sheirer
366055145e #1498 Updates JMBE audio module to retain class reference instead of reflectively loading the class each time for a new instance. Reduces pressure on both the class loader and the garbage collector. (#1499)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-20 19:56:57 -04:00
Denny Sheirer
39ebdb47b1 #1491 Adds diagnostic logging to dispatcher to support troubleshooting polyphase channel manager's dispatch thread being blocked. (#1492)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-12 09:21:37 -04:00
Denny Sheirer
c788292db8 #1489 SDRPlay RSP tuner editors now lock sample rate combo when sourcing channels. (#1490)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-12 07:21:23 -04:00
Denny Sheirer
f0dc80437c #1487 Updates hot code classes to remove usage of reentrant locks for thread synchronization. (#1488)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 16:04:55 -05:00
Denny Sheirer
2d6c5c2331 #1485 Sets Blackman-Harris 7 As Default FFT Window (#1486)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 15:05:25 -05:00
Denny Sheirer
bc4e6cb5b0 #1473 Adds IF AGC Control for RSP tuners and Power Overload indication. Reverts changes to default MacOS library location in API. (#1484)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 14:58:58 -05:00
Denny Sheirer
d69ad46c10 #1471 Handling of unknown Hytera packet messages. (#1483)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-03-11 06:04:06 -05:00
Denny Sheirer
ba7b616f88 #1405 resolves issue with polyphase channel thread not shutting down correctly. Resolves issue with building distro releases using non-local JDKs. (#1477)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2023-03-05 05:55:01 -05:00
Denny Sheirer
8355b7e3b9 #1471 Adds support for Hytera Proprietary Long Data Service SMS messages. Resolves issue parsing Multi-Block CSBKs. (#1472)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-26 10:23:00 -05:00
Denny Sheirer
22ffb656be #1464 Reduces logging when SDRplay API is not installed on local machine. (#1465)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-20 12:10:17 -05:00
Denny Sheirer
de80d2a6c1 #1459 Updates gradle build to fix bug related to building releases for each OS (#1463)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-20 12:04:48 -05:00
Denny Sheirer
a320b4a24f #1461 Updates expected sdrplay library path for OSX (#1462)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-20 04:55:50 -05:00
816 changed files with 41599 additions and 13075 deletions

38
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug Report
about: Create a report to help us improve sdrtrunk
title: ''
labels: bug
assignees: ''
---
**sdrtrunk Version**
Indicate which sdrtrunk version you are using. Use 'master-branch' if you are running the latest code.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Application Log**
If applicable, include either the full application log,or a snippet of the error logging.
**Desktop (optional - complete the following information):**
- OS: [e.g. iOS]
- CPU Cores: [e.g. 4 cores]
- RAM:[e.g. 16GB]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,16 @@
---
name: How-To Question or Does The Software Support xxx
about: End user question or support discussion
title: STOP - Don't Use Issue Reporting to Post End-User Questions!!!
labels: ''
assignees: ''
---
STOP !!!!
GitHub issues are for reporting software bugs or requesting enhancements.
If you have a question about how to use the software, or if you want to know if sdrtrunk supports a protocol, tuner, or other aspect, please post your question in one of the Support forums.
Visit: https://github.com/DSheirer/sdrtrunk/wiki

View File

@@ -21,14 +21,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Set up JDK 19
uses: actions/setup-java@v2
- uses: actions/checkout@v4
- name: Set up JDK 20
uses: actions/setup-java@v3
with:
java-version: '19'
java-version: '20'
distribution: 'liberica'
java-package: 'jdk+fx'
- name: Build with Gradle
uses: gradle/gradle-build-action@937999e9cc2425eddc7fd62d1053baf041147db7
uses: gradle/gradle-build-action@v2
with:
arguments: clean build

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_19_PREVIEW" project-jdk-name="liberica-19.0.1.fx" 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

@@ -0,0 +1,230 @@
#ifndef SDRPLAY_API_H
#define SDRPLAY_API_H
#include "sdrplay_api_dev.h"
#include "sdrplay_api_rx_channel.h"
#include "sdrplay_api_callback.h"
//#if defined(_M_X64) || defined(_M_IX86)
//#include "windows.h"
//#elif defined (__GNUC__)
typedef void *HANDLE;
//#endif
//
//#ifndef _SDRPLAY_DLL_QUALIFIER
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86))
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
//#elif defined(STATIC_LIB) || defined(__GNUC__)
//#define _SDRPLAY_DLL_QUALIFIER
//#endif
//#endif // _SDRPLAY_DLL_QUALIFIER
// Application code should check that it is compiled against the same API version
// sdrplay_api_ApiVersion() returns the API version
#define SDRPLAY_API_VERSION (float)(3.08)
// API Constants
#define SDRPLAY_MAX_DEVICES (16)
#define SDRPLAY_MAX_TUNERS_PER_DEVICE (2)
#define SDRPLAY_MAX_SER_NO_LEN (64)
#define SDRPLAY_MAX_ROOT_NM_LEN (32)
#define SDRPLAY_RSP1_ID (1)
#define SDRPLAY_RSP1A_ID (255)
#define SDRPLAY_RSP2_ID (2)
#define SDRPLAY_RSPduo_ID (3)
#define SDRPLAY_RSPdx_ID (4)
// Enum types
typedef enum
{
sdrplay_api_Success = 0,
sdrplay_api_Fail = 1,
sdrplay_api_InvalidParam = 2,
sdrplay_api_OutOfRange = 3,
sdrplay_api_GainUpdateError = 4,
sdrplay_api_RfUpdateError = 5,
sdrplay_api_FsUpdateError = 6,
sdrplay_api_HwError = 7,
sdrplay_api_AliasingError = 8,
sdrplay_api_AlreadyInitialised = 9,
sdrplay_api_NotInitialised = 10,
sdrplay_api_NotEnabled = 11,
sdrplay_api_HwVerError = 12,
sdrplay_api_OutOfMemError = 13,
sdrplay_api_ServiceNotResponding = 14,
sdrplay_api_StartPending = 15,
sdrplay_api_StopPending = 16,
sdrplay_api_InvalidMode = 17,
sdrplay_api_FailedVerification1 = 18,
sdrplay_api_FailedVerification2 = 19,
sdrplay_api_FailedVerification3 = 20,
sdrplay_api_FailedVerification4 = 21,
sdrplay_api_FailedVerification5 = 22,
sdrplay_api_FailedVerification6 = 23,
sdrplay_api_InvalidServiceVersion = 24
} sdrplay_api_ErrT;
typedef enum
{
sdrplay_api_Update_None = 0x00000000,
// Reasons for master only mode
sdrplay_api_Update_Dev_Fs = 0x00000001,
sdrplay_api_Update_Dev_Ppm = 0x00000002,
sdrplay_api_Update_Dev_SyncUpdate = 0x00000004,
sdrplay_api_Update_Dev_ResetFlags = 0x00000008,
sdrplay_api_Update_Rsp1a_BiasTControl = 0x00000010,
sdrplay_api_Update_Rsp1a_RfNotchControl = 0x00000020,
sdrplay_api_Update_Rsp1a_RfDabNotchControl = 0x00000040,
sdrplay_api_Update_Rsp2_BiasTControl = 0x00000080,
sdrplay_api_Update_Rsp2_AmPortSelect = 0x00000100,
sdrplay_api_Update_Rsp2_AntennaControl = 0x00000200,
sdrplay_api_Update_Rsp2_RfNotchControl = 0x00000400,
sdrplay_api_Update_Rsp2_ExtRefControl = 0x00000800,
sdrplay_api_Update_RspDuo_ExtRefControl = 0x00001000,
sdrplay_api_Update_Master_Spare_1 = 0x00002000,
sdrplay_api_Update_Master_Spare_2 = 0x00004000,
// Reasons for master and slave mode
// Note: sdrplay_api_Update_Tuner_Gr MUST be the first value defined in this section!
sdrplay_api_Update_Tuner_Gr = 0x00008000,
sdrplay_api_Update_Tuner_GrLimits = 0x00010000,
sdrplay_api_Update_Tuner_Frf = 0x00020000,
sdrplay_api_Update_Tuner_BwType = 0x00040000,
sdrplay_api_Update_Tuner_IfType = 0x00080000,
sdrplay_api_Update_Tuner_DcOffset = 0x00100000,
sdrplay_api_Update_Tuner_LoMode = 0x00200000,
sdrplay_api_Update_Ctrl_DCoffsetIQimbalance = 0x00400000,
sdrplay_api_Update_Ctrl_Decimation = 0x00800000,
sdrplay_api_Update_Ctrl_Agc = 0x01000000,
sdrplay_api_Update_Ctrl_AdsbMode = 0x02000000,
sdrplay_api_Update_Ctrl_OverloadMsgAck = 0x04000000,
sdrplay_api_Update_RspDuo_BiasTControl = 0x08000000,
sdrplay_api_Update_RspDuo_AmPortSelect = 0x10000000,
sdrplay_api_Update_RspDuo_Tuner1AmNotchControl = 0x20000000,
sdrplay_api_Update_RspDuo_RfNotchControl = 0x40000000,
sdrplay_api_Update_RspDuo_RfDabNotchControl = 0x80000000,
} sdrplay_api_ReasonForUpdateT;
typedef enum
{
sdrplay_api_Update_Ext1_None = 0x00000000,
// Reasons for master only mode
sdrplay_api_Update_RspDx_HdrEnable = 0x00000001,
sdrplay_api_Update_RspDx_BiasTControl = 0x00000002,
sdrplay_api_Update_RspDx_AntennaControl = 0x00000004,
sdrplay_api_Update_RspDx_RfNotchControl = 0x00000008,
sdrplay_api_Update_RspDx_RfDabNotchControl = 0x00000010,
sdrplay_api_Update_RspDx_HdrBw = 0x00000020,
// Reasons for master and slave mode
} sdrplay_api_ReasonForUpdateExtension1T;
typedef enum
{
sdrplay_api_DbgLvl_Disable = 0,
sdrplay_api_DbgLvl_Verbose = 1,
sdrplay_api_DbgLvl_Warning = 2,
sdrplay_api_DbgLvl_Error = 3,
sdrplay_api_DbgLvl_Message = 4,
} sdrplay_api_DbgLvl_t;
// Device structure
typedef struct
{
char SerNo[SDRPLAY_MAX_SER_NO_LEN];
unsigned char hwVer;
sdrplay_api_TunerSelectT tuner;
sdrplay_api_RspDuoModeT rspDuoMode;
unsigned char valid;
double rspDuoSampleFreq;
HANDLE dev;
} sdrplay_api_DeviceT;
// Device parameter structure
typedef struct
{
sdrplay_api_DevParamsT *devParams;
sdrplay_api_RxChannelParamsT *rxChannelA;
sdrplay_api_RxChannelParamsT *rxChannelB;
} sdrplay_api_DeviceParamsT;
// Extended error message structure
typedef struct
{
char file[256];
char function[256];
int line;
char message[1024];
} sdrplay_api_ErrorInfoT;
// Comman API function types
typedef sdrplay_api_ErrT (*sdrplay_api_Open_t)(void);
typedef sdrplay_api_ErrT (*sdrplay_api_Close_t)(void);
typedef sdrplay_api_ErrT (*sdrplay_api_ApiVersion_t)(float *apiVer);
typedef sdrplay_api_ErrT (*sdrplay_api_LockDeviceApi_t)(void);
typedef sdrplay_api_ErrT (*sdrplay_api_UnlockDeviceApi_t)(void);
typedef sdrplay_api_ErrT (*sdrplay_api_GetDevices_t)(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
typedef sdrplay_api_ErrT (*sdrplay_api_SelectDevice_t)(sdrplay_api_DeviceT *device);
typedef sdrplay_api_ErrT (*sdrplay_api_ReleaseDevice_t)(sdrplay_api_DeviceT *device);
typedef const char* (*sdrplay_api_GetErrorString_t)(sdrplay_api_ErrT err);
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastError_t)(sdrplay_api_DeviceT *device);
typedef sdrplay_api_ErrT (*sdrplay_api_DisableHeartbeat_t)(void);
// Device API function types
typedef sdrplay_api_ErrT (*sdrplay_api_DebugEnable_t)(HANDLE dev, sdrplay_api_DbgLvl_t dbgLvl);
typedef sdrplay_api_ErrT (*sdrplay_api_GetDeviceParams_t)(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
typedef sdrplay_api_ErrT (*sdrplay_api_Init_t)(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
typedef sdrplay_api_ErrT (*sdrplay_api_Uninit_t)(HANDLE dev);
typedef sdrplay_api_ErrT (*sdrplay_api_Update_t)(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoActiveTuner_t)(HANDLE dev, sdrplay_api_TunerSelectT *tuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoDualTunerModeSampleRate_t)(double *currentSampleRate, double newSampleRate);
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoMode_t)(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
// API function definitions
#ifdef __cplusplus
extern "C"
{
#endif
// // Comman API function definitions
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Open(void);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Close(void);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
// _SDRPLAY_DLL_QUALIFIER const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
//
// // Device API function definitions
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
// sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
// sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
#ifdef __cplusplus
}
#endif
#endif //SDRPLAY_API_H

View File

@@ -0,0 +1,78 @@
#ifndef SDRPLAY_API_CALLBACK_H
#define SDRPLAY_API_CALLBACK_H
// Event callback enums
typedef enum
{
sdrplay_api_Overload_Detected = 0,
sdrplay_api_Overload_Corrected = 1,
} sdrplay_api_PowerOverloadCbEventIdT;
typedef enum
{
sdrplay_api_MasterInitialised = 0,
sdrplay_api_SlaveAttached = 1,
sdrplay_api_SlaveDetached = 2,
sdrplay_api_SlaveInitialised = 3,
sdrplay_api_SlaveUninitialised = 4,
sdrplay_api_MasterDllDisappeared = 5,
sdrplay_api_SlaveDllDisappeared = 6,
} sdrplay_api_RspDuoModeCbEventIdT;
typedef enum
{
sdrplay_api_GainChange = 0,
sdrplay_api_PowerOverloadChange = 1,
sdrplay_api_DeviceRemoved = 2,
sdrplay_api_RspDuoModeChange = 3,
} sdrplay_api_EventT;
// Event callback parameter structs
typedef struct
{
unsigned int gRdB;
unsigned int lnaGRdB;
double currGain;
} sdrplay_api_GainCbParamT;
typedef struct
{
sdrplay_api_PowerOverloadCbEventIdT powerOverloadChangeType;
} sdrplay_api_PowerOverloadCbParamT;
typedef struct
{
sdrplay_api_RspDuoModeCbEventIdT modeChangeType;
} sdrplay_api_RspDuoModeCbParamT;
// Event parameters overlay
typedef union
{
sdrplay_api_GainCbParamT gainParams;
sdrplay_api_PowerOverloadCbParamT powerOverloadParams;
sdrplay_api_RspDuoModeCbParamT rspDuoModeParams;
} sdrplay_api_EventParamsT;
// Stream callback parameter structs
typedef struct
{
unsigned int firstSampleNum;
int grChanged;
int rfChanged;
int fsChanged;
unsigned int numSamples;
} sdrplay_api_StreamCbParamsT;
// Callback function prototypes
typedef void (*sdrplay_api_StreamCallback_t)(short *xi, short *xq, sdrplay_api_StreamCbParamsT *params, unsigned int numSamples, unsigned int reset, void *cbContext);
typedef void (*sdrplay_api_EventCallback_t)(sdrplay_api_EventT eventId, sdrplay_api_TunerSelectT tuner, sdrplay_api_EventParamsT *params, void *cbContext);
// Callback function struct
typedef struct
{
sdrplay_api_StreamCallback_t StreamACbFn;
sdrplay_api_StreamCallback_t StreamBCbFn;
sdrplay_api_EventCallback_t EventCbFn;
} sdrplay_api_CallbackFnsT;
#endif //SDRPLAY_API_CALLBACK_H

View File

@@ -0,0 +1,55 @@
#ifndef SDRPLAY_API_CONTROL_H
#define SDRPLAY_API_CONTROL_H
// Control parameter enums
typedef enum
{
sdrplay_api_AGC_DISABLE = 0,
sdrplay_api_AGC_100HZ = 1,
sdrplay_api_AGC_50HZ = 2,
sdrplay_api_AGC_5HZ = 3,
sdrplay_api_AGC_CTRL_EN = 4
} sdrplay_api_AgcControlT;
typedef enum
{
sdrplay_api_ADSB_DECIMATION = 0,
sdrplay_api_ADSB_NO_DECIMATION_LOWPASS = 1,
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_2MHZ = 2,
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_3MHZ = 3
} sdrplay_api_AdsbModeT;
// Control parameter structs
typedef struct
{
unsigned char DCenable; // default: 1
unsigned char IQenable; // default: 1
} sdrplay_api_DcOffsetT;
typedef struct
{
unsigned char enable; // default: 0
unsigned char decimationFactor; // default: 1
unsigned char wideBandSignal; // default: 0
} sdrplay_api_DecimationT;
typedef struct
{
sdrplay_api_AgcControlT enable; // default: sdrplay_api_AGC_50HZ
int setPoint_dBfs; // default: -60
unsigned short attack_ms; // default: 0
unsigned short decay_ms; // default: 0
unsigned short decay_delay_ms; // default: 0
unsigned short decay_threshold_dB; // default: 0
int syncUpdate; // default: 0
} sdrplay_api_AgcT;
typedef struct
{
sdrplay_api_DcOffsetT dcOffset;
sdrplay_api_DecimationT decimation;
sdrplay_api_AgcT agc;
sdrplay_api_AdsbModeT adsbMode; //default: sdrplay_api_ADSB_DECIMATION
} sdrplay_api_ControlParamsT;
#endif //SDRPLAY_API_CONTROL_H

View File

@@ -0,0 +1,51 @@
#ifndef SDRPLAY_API_DEV_H
#define SDRPLAY_API_DEV_H
#include "sdrplay_api_rsp1a.h"
#include "sdrplay_api_rsp2.h"
#include "sdrplay_api_rspDuo.h"
#include "sdrplay_api_rspDx.h"
// Dev parameter enums
typedef enum
{
sdrplay_api_ISOCH = 0,
sdrplay_api_BULK = 1
} sdrplay_api_TransferModeT;
// Dev parameter structs
typedef struct
{
double fsHz; // default: 2000000.0
unsigned char syncUpdate; // default: 0
unsigned char reCal; // default: 0
} sdrplay_api_FsFreqT;
typedef struct
{
unsigned int sampleNum; // default: 0
unsigned int period; // default: 0
} sdrplay_api_SyncUpdateT;
typedef struct
{
unsigned char resetGainUpdate; // default: 0
unsigned char resetRfUpdate; // default: 0
unsigned char resetFsUpdate; // default: 0
} sdrplay_api_ResetFlagsT;
typedef struct
{
double ppm; // default: 0.0
sdrplay_api_FsFreqT fsFreq;
sdrplay_api_SyncUpdateT syncUpdate;
sdrplay_api_ResetFlagsT resetFlags;
sdrplay_api_TransferModeT mode; // default: sdrplay_api_ISOCH
unsigned int samplesPerPkt; // default: 0 (output param)
sdrplay_api_Rsp1aParamsT rsp1aParams;
sdrplay_api_Rsp2ParamsT rsp2Params;
sdrplay_api_RspDuoParamsT rspDuoParams;
sdrplay_api_RspDxParamsT rspDxParams;
} sdrplay_api_DevParamsT;
#endif //SDRPLAY_API_DEV_H

View File

@@ -0,0 +1,22 @@
#ifndef SDRPLAY_API_RSP1A_H
#define SDRPLAY_API_RSP1A_H
#define RSPIA_NUM_LNA_STATES 10
#define RSPIA_NUM_LNA_STATES_AM 7
#define RSPIA_NUM_LNA_STATES_LBAND 9
// RSP1A parameter enums
// RSP1A parameter structs
typedef struct
{
unsigned char rfNotchEnable; // default: 0
unsigned char rfDabNotchEnable; // default: 0
} sdrplay_api_Rsp1aParamsT;
typedef struct
{
unsigned char biasTEnable; // default: 0
} sdrplay_api_Rsp1aTunerParamsT;
#endif //SDRPLAY_API_RSP1A_H

View File

@@ -0,0 +1,35 @@
#ifndef SDRPLAY_API_RSP2_H
#define SDRPLAY_API_RSP2_H
#define RSPII_NUM_LNA_STATES 9
#define RSPII_NUM_LNA_STATES_AMPORT 5
#define RSPII_NUM_LNA_STATES_420MHZ 6
// RSP2 parameter enums
typedef enum
{
sdrplay_api_Rsp2_ANTENNA_A = 5,
sdrplay_api_Rsp2_ANTENNA_B = 6,
} sdrplay_api_Rsp2_AntennaSelectT;
typedef enum
{
sdrplay_api_Rsp2_AMPORT_1 = 1,
sdrplay_api_Rsp2_AMPORT_2 = 0,
} sdrplay_api_Rsp2_AmPortSelectT;
// RSP2 parameter structs
typedef struct
{
unsigned char extRefOutputEn; // default: 0
} sdrplay_api_Rsp2ParamsT;
typedef struct
{
unsigned char biasTEnable; // default: 0
sdrplay_api_Rsp2_AmPortSelectT amPortSel; // default: sdrplay_api_Rsp2_AMPORT_2
sdrplay_api_Rsp2_AntennaSelectT antennaSel; // default: sdrplay_api_Rsp2_ANTENNA_A
unsigned char rfNotchEnable; // default: 0
} sdrplay_api_Rsp2TunerParamsT;
#endif //SDRPLAY_API_RSP2_H

View File

@@ -0,0 +1,40 @@
#ifndef SDRPLAY_API_RSPduo_H
#define SDRPLAY_API_RSPduo_H
#define RSPDUO_NUM_LNA_STATES 10
#define RSPDUO_NUM_LNA_STATES_AMPORT 5
#define RSPDUO_NUM_LNA_STATES_AM 7
#define RSPDUO_NUM_LNA_STATES_LBAND 9
// RSPduo parameter enums
typedef enum
{
sdrplay_api_RspDuoMode_Unknown = 0,
sdrplay_api_RspDuoMode_Single_Tuner = 1,
sdrplay_api_RspDuoMode_Dual_Tuner = 2,
sdrplay_api_RspDuoMode_Master = 4,
sdrplay_api_RspDuoMode_Slave = 8,
} sdrplay_api_RspDuoModeT;
typedef enum
{
sdrplay_api_RspDuo_AMPORT_1 = 1,
sdrplay_api_RspDuo_AMPORT_2 = 0,
} sdrplay_api_RspDuo_AmPortSelectT;
// RSPduo parameter structs
typedef struct
{
int extRefOutputEn; // default: 0
} sdrplay_api_RspDuoParamsT;
typedef struct
{
unsigned char biasTEnable; // default: 0
sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel; // default: sdrplay_api_RspDuo_AMPORT_2
unsigned char tuner1AmNotchEnable; // default: 0
unsigned char rfNotchEnable; // default: 0
unsigned char rfDabNotchEnable; // default: 0
} sdrplay_api_RspDuoTunerParamsT;
#endif //SDRPLAY_API_RSPduo_H

View File

@@ -0,0 +1,45 @@
#ifndef SDRPLAY_API_RSPDX_H
#define SDRPLAY_API_RSPDX_H
#include "sdrplay_api_tuner.h"
#define RSPDX_NUM_LNA_STATES 28 // Number of LNA states in all bands (except where defined differently below)
#define RSPDX_NUM_LNA_STATES_AMPORT2_0_12 19 // Number of LNA states when using AM Port 2 between 0 and 12MHz
#define RSPDX_NUM_LNA_STATES_AMPORT2_12_60 20 // Number of LNA states when using AM Port 2 between 12 and 60MHz
#define RSPDX_NUM_LNA_STATES_VHF_BAND3 27 // Number of LNA states in VHF and Band3
#define RSPDX_NUM_LNA_STATES_420MHZ 21 // Number of LNA states in 420MHz band
#define RSPDX_NUM_LNA_STATES_LBAND 19 // Number of LNA states in L-band
#define RSPDX_NUM_LNA_STATES_DX 26 // Number of LNA states in DX path
// RSPdx parameter enums
typedef enum
{
sdrplay_api_RspDx_ANTENNA_A = 0,
sdrplay_api_RspDx_ANTENNA_B = 1,
sdrplay_api_RspDx_ANTENNA_C = 2,
} sdrplay_api_RspDx_AntennaSelectT;
typedef enum
{
sdrplay_api_RspDx_HDRMODE_BW_0_200 = 0,
sdrplay_api_RspDx_HDRMODE_BW_0_500 = 1,
sdrplay_api_RspDx_HDRMODE_BW_1_200 = 2,
sdrplay_api_RspDx_HDRMODE_BW_1_700 = 3,
} sdrplay_api_RspDx_HdrModeBwT;
// RSPdx parameter structs
typedef struct
{
unsigned char hdrEnable; // default: 0
unsigned char biasTEnable; // default: 0
sdrplay_api_RspDx_AntennaSelectT antennaSel; // default: sdrplay_api_RspDx_ANTENNA_A
unsigned char rfNotchEnable; // default: 0
unsigned char rfDabNotchEnable; // default: 0
} sdrplay_api_RspDxParamsT;
typedef struct
{
sdrplay_api_RspDx_HdrModeBwT hdrBw; // default: sdrplay_api_RspDx_HDRMODE_BW_1_700
} sdrplay_api_RspDxTunerParamsT;
#endif //SDRPLAY_API_RSPDX_H

View File

@@ -0,0 +1,21 @@
#ifndef SDRPLAY_API_RX_CHANNEL_H
#define SDRPLAY_API_RX_CHANNEL_H
#include "sdrplay_api_tuner.h"
#include "sdrplay_api_control.h"
#include "sdrplay_api_rsp1a.h"
#include "sdrplay_api_rsp2.h"
#include "sdrplay_api_rspDuo.h"
#include "sdrplay_api_rspDx.h"
typedef struct
{
sdrplay_api_TunerParamsT tunerParams;
sdrplay_api_ControlParamsT ctrlParams;
sdrplay_api_Rsp1aTunerParamsT rsp1aTunerParams;
sdrplay_api_Rsp2TunerParamsT rsp2TunerParams;
sdrplay_api_RspDuoTunerParamsT rspDuoTunerParams;
sdrplay_api_RspDxTunerParamsT rspDxTunerParams;
} sdrplay_api_RxChannelParamsT;
#endif //SDRPLAY_API_RX_CHANNEL_H

View File

@@ -0,0 +1,93 @@
#ifndef SDRPLAY_API_TUNER_H
#define SDRPLAY_API_TUNER_H
#define MAX_BB_GR (59)
// Tuner parameter enums
typedef enum
{
sdrplay_api_BW_Undefined = 0,
sdrplay_api_BW_0_200 = 200,
sdrplay_api_BW_0_300 = 300,
sdrplay_api_BW_0_600 = 600,
sdrplay_api_BW_1_536 = 1536,
sdrplay_api_BW_5_000 = 5000,
sdrplay_api_BW_6_000 = 6000,
sdrplay_api_BW_7_000 = 7000,
sdrplay_api_BW_8_000 = 8000
} sdrplay_api_Bw_MHzT;
typedef enum
{
sdrplay_api_IF_Undefined = -1,
sdrplay_api_IF_Zero = 0,
sdrplay_api_IF_0_450 = 450,
sdrplay_api_IF_1_620 = 1620,
sdrplay_api_IF_2_048 = 2048
} sdrplay_api_If_kHzT;
typedef enum
{
sdrplay_api_LO_Undefined = 0,
sdrplay_api_LO_Auto = 1,
sdrplay_api_LO_120MHz = 2,
sdrplay_api_LO_144MHz = 3,
sdrplay_api_LO_168MHz = 4
} sdrplay_api_LoModeT;
typedef enum
{
sdrplay_api_EXTENDED_MIN_GR = 0,
sdrplay_api_NORMAL_MIN_GR = 20
} sdrplay_api_MinGainReductionT;
typedef enum
{
sdrplay_api_Tuner_Neither = 0,
sdrplay_api_Tuner_A = 1,
sdrplay_api_Tuner_B = 2,
sdrplay_api_Tuner_Both = 3,
} sdrplay_api_TunerSelectT;
// Tuner parameter structs
typedef struct
{
float curr;
float max;
float min;
} sdrplay_api_GainValuesT;
typedef struct
{
int gRdB; // default: 50
unsigned char LNAstate; // default: 0
unsigned char syncUpdate; // default: 0
sdrplay_api_MinGainReductionT minGr; // default: sdrplay_api_NORMAL_MIN_GR
sdrplay_api_GainValuesT gainVals; // output parameter
} sdrplay_api_GainT;
typedef struct
{
double rfHz; // default: 200000000.0
unsigned char syncUpdate; // default: 0
} sdrplay_api_RfFreqT;
typedef struct
{
unsigned char dcCal; // default: 3 (Periodic mode)
unsigned char speedUp; // default: 0 (No speedup)
int trackTime; // default: 1 (=> time in uSec = (72 * 3 * trackTime) / 24e6 = 9uSec)
int refreshRateTime; // default: 2048 (=> time in uSec = (72 * 3 * refreshRateTime) / 24e6 = 18432uSec)
} sdrplay_api_DcOffsetTunerT;
typedef struct
{
sdrplay_api_Bw_MHzT bwType; // default: sdrplay_api_BW_0_200
sdrplay_api_If_kHzT ifType; // default: sdrplay_api_IF_Zero
sdrplay_api_LoModeT loMode; // default: sdrplay_api_LO_Auto
sdrplay_api_GainT gain;
sdrplay_api_RfFreqT rfFreq;
sdrplay_api_DcOffsetTunerT dcOffsetTuner;
} sdrplay_api_TunerParamsT;
#endif //SDRPLAY_API_TUNER_H

View File

@@ -2,7 +2,7 @@ import java.text.SimpleDateFormat
/*
* *****************************************************************************
* 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,6 +18,29 @@ import java.text.SimpleDateFormat
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
/**
* 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+
*
* Scenario 1: run the application via gradle command line from the source code root directory:
* command: ./gradlew run
*
* Scenario 2: create an application release for the current operating system
* command: ./gradlew runtimeZipCurrent
* Note: release image is located in the /build/image/ directory
*
* Scenario 3: create releases for Linux and OSX operating systems using downloaded JDKs
* command: ./gradlew runtimeZipOthers
* Note: release images are located in the /build/image/ directory
*
* Scenario 4: create release for Windows operating system using downloaded JDK
* command: ./gradlew runtimeZipWindows
* Note: release image is located in the /build/image/ directory
*/
plugins {
id 'application'
id 'java'
@@ -30,12 +53,13 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.6.0-alpha2'
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 19 is required for this version of the Project Panama preview/incubator feature
//Java 20 is required for this version of the Project Panama preview/incubator feature
java {
toolchain {
languageVersion = JavaLanguageVersion.of(19)
languageVersion = JavaLanguageVersion.of(20)
}
}
@@ -98,6 +122,8 @@ dependencies {
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
def os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem
/**
* This is needed for the JDK19 panama and vector API ... until it moves out of incubation or preview
*/
@@ -106,24 +132,35 @@ tasks.withType(JavaCompile) {
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Needed while Panama Vector API remains in incubator
}
application {
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
applicationDefaultJvmArgs =
/**
* JVM Arguments for running the application on Windows.
*/
def jvmArgsWindows =
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
'--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED', //windows users for JIDE
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
'--enable-native-access=ALL-UNNAMED',
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //Allows Project Panama Foreign Function access to system library
/**
* JVM Arguments for running the application on Linux.
*/
def jvmArgsLinux =
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
'--enable-native-access=ALL-UNNAMED'] //Allows Project Panama Foreign Function access to system library
'--enable-native-access=ALL-UNNAMED',
'-Djava.library.path=/usr/local/lib'] //Allows Project Panama Foreign Function access to system library
application {
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
OperatingSystem os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem;
if(os.isWindows()) {
//Replace the default Java system library path with an explicit location for the SDRplay API library (Windows)
applicationDefaultJvmArgs.add('-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64')
//On windows, we also need to export the windows swing look & feel module
applicationDefaultJvmArgs.add('--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED')
applicationDefaultJvmArgs = jvmArgsWindows
}
else {
//Replace the default Java system library path with an explicit location for the SDRplay API library (Linux/OS-X)
applicationDefaultJvmArgs.add("-Djava.library.path=/usr/local/lib")
applicationDefaultJvmArgs = jvmArgsLinux
}
}
@@ -161,91 +198,158 @@ String targetWindowsX86_64 = 'windows-x86_64-v' + version
* author's development environment, but can be changed to your environment if you want to use this build pattern.
*/
String jdk_base = '/home/denny/java_jdks/'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-19.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-19.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-19.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-19.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-19.0.1-full'
boolean hasTargetJdks = file(jdk_linux_x86_64).exists() || file(jdk_linux_aarch64).exists() ||
file(jdk_osx_x86_64).exists() || file(jdk_osx_aarch64).exists() || file(jdk_windows_x86_64).exists()
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-20.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-20.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-20.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-20.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-20.0.1-full'
/**
* Auto-download target JDKs. Defaults to false, or the user can override via the gradle command line by
* including this option: '-PdownloadJdks=true'
* Download URLs to download the JDK as part of the gradle build packaging process
*/
boolean downloadJdks = project.hasProperty('downloadJdks') ? project.getProperty('downloadJdks') : false
def jdk_download_base = "https://download.bell-sw.com/java/20.0.1+10/bellsoft-jdk20.0.1+10-"
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-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"
/**
* Beryx Runtime plugin for creating OS and CPU targeted build(s) of the software.
*
* Option 1: Create a single target build using the locally installed JDK, or optionally create target builds for
* all OS and CPU targets when pre-downloaded target JDKs are available (see 'jdk_*' properties above).
*
* >: gradle runtimeZip
*
* Option 2: Create all OS and CPU target builds by automatically downloading all JDKs needed to build the target images
*
* >: gradle runtimeZip -PdownloadJdks=true
* Configures the runtime zip task with additional options/settings.
* @param rt task to configure
* @param jvmArgs to use (windows or linux)
* @return configured task.
*/
runtime {
//Build by automatically downloading JDKs for the OS and CPU targets
if(downloadJdks) {
println("Building all runtime images")
println("The [ > jre: ] build phase downloads several target JDKs and this may take a while ... please wait")
targetPlatform(targetLinuxAarch64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-linux-aarch64-full.tar.gz")
}
targetPlatform(targetLinuxX86_64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-linux-amd64-full.tar.gz")
}
targetPlatform(targetOsxAarch64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-macos-aarch64-full.tar.gz")
}
targetPlatform(targetOsxX86_64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-macos-amd64-full.tar.gz")
}
targetPlatform(targetWindowsX86_64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-windows-amd64-full.zip")
}
}
//Build using pre-downloaded JDKs
else if(hasTargetJdks) {
println("Building all runtime images using pre-downloaded JDKs")
if(file(jdk_linux_aarch64).exists()) {
targetPlatform(targetLinuxAarch64, jdk_linux_aarch64)
}
if(file(jdk_linux_x86_64).exists()) {
targetPlatform(targetLinuxX86_64, jdk_linux_x86_64)
}
if(file(jdk_osx_aarch64).exists()) {
targetPlatform(targetOsxAarch64, jdk_osx_aarch64)
}
if(file(jdk_osx_x86_64).exists()) {
targetPlatform(targetOsxX86_64, jdk_osx_x86_64)
}
if(file(jdk_windows_x86_64).exists()) {
targetPlatform(targetWindowsX86_64, jdk_windows_x86_64)
}
}
//Build using locally installed JDK only
else {
println("Building a single runtime image using locally installed JDK")
}
def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
//Use the correct set of JVM arguments
rt.extension.launcherData.get().jvmArgs = jvmArgs
//Additional modules to include with auto-detected modules.
//jdk.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
modules = ['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')
//Use auto-detected modules and 'add' any specified modules.
additive = true
rt.extension.additive.set(true)
//Recommended options for reducing/shrinking runtime target image size
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages']
rt.extension.addOptions('--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages')
//Runtime images have auto appended version in image zip name. Override to include version in single target image.
imageZip = (hasTargetJdks | downloadJdks) ? file("$buildDir/image/sdr-trunk.zip") :
file("$buildDir/image/sdr-trunk-" + version + ".zip")
rt.extension.imageZip.set(file("$buildDir/image/sdr-trunk.zip"))
}
/**
* Creates a release package for the current operating system.
*
* Note: you cannot create both Windows and Linux releases at the same time due to limitations of the Runtime plugin
*
* Usage: gradle runtimeZipCurrent
* Usage: gradlew runtimeZipCurrent (windows - using gradlew)
* Usage: ./gradlew runtimeZipCurrent (linux - using gradlew)
*/
tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
def arch = System.properties.getProperty("os.arch")
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
if(os.isWindows()) {
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
configure(rt, jvmArgsWindows)
}
else if(os.isLinux())
{
if(arch.equals("amd64")) {
rt.extension.targetPlatform(targetLinuxX86_64, javaHome)
}
else if(arch.equals("aarch64")) {
rt.extension.targetPlatform(targetLinuxAarch64, javaHome)
}
configure(rt, jvmArgsLinux)
}
else if(os.isMacOsX())
{
if(arch.equals("amd64")) {
rt.extension.targetPlatform(targetOsxX86_64, javaHome)
}
else if(arch.equals("aarch64")) {
rt.extension.targetPlatform(targetOsxAarch64, javaHome)
}
configure(rt, jvmArgsLinux)
}
}
/**
* Creates a Windows release package.
*
* Note: you cannot create both Windows and Linux releases at the same time due to limitations of the Runtime plugin
*
* Usage: gradle runtimeZipWindows
* Usage: gradlew runtimeZipWindows (windows - using gradlew)
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
*/
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
if(file(jdk_windows_x86_64).exists()) {
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
}
else {
rt.extension.targetPlatform(targetWindowsX86_64) {
jdkHome = jdkDownload(jdk_download_windows_x86_64)
}
}
configure(rt, jvmArgsWindows)
}
/**
* Creates Linux & OSX release packages.
*
* Note: you cannot create both Windows and Linux releases at the same time due to limitations of the Runtime plugin
*
* Usage: gradle runtimeZipOthers
* Usage: gradlew runtimeZipOthers (windows - using gradlew)
* Usage: ./gradlew runtimeZipOthers (linux - using gradlew)
*/
tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
//Linux aarch64
if(file(jdk_linux_aarch64).exists()) {
rt.extension.targetPlatform(targetLinuxAarch64, jdk_linux_aarch64, [])
}
else {
rt.extension.targetPlatform(targetLinuxAarch64) {
jdkHome = jdkDownload(jdk_download_linux_aarch64)
}
}
//Linux x86-64
if(file(jdk_linux_x86_64).exists()) {
rt.extension.targetPlatform(targetLinuxX86_64, jdk_linux_x86_64, [])
}
else {
rt.extension.targetPlatform(targetLinuxX86_64) {
jdkHome = jdkDownload(jdk_download_linux_x86_64)
}
}
//OSX aarch64
if(file(jdk_osx_aarch64).exists()) {
rt.extension.targetPlatform(targetOsxAarch64, jdk_osx_aarch64, [])
}
else {
rt.extension.targetPlatform(targetOsxAarch64) {
jdkHome = jdkDownload(jdk_download_osx_aarch64)
}
}
//OSX x86-64
if(file(jdk_osx_x86_64).exists()) {
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
}
else {
rt.extension.targetPlatform(targetOsxX86_64) {
jdkHome = jdkDownload(jdk_download_osx_x86_64)
}
}
configure(rt, jvmArgsLinux);
}

31
gradle.properties Normal file
View File

@@ -0,0 +1,31 @@
#
# ******************************************************************************
# 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/>
# *****************************************************************************
#
# 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.6.0

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,7 +19,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

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,37 +19,31 @@
package io.github.dsheirer.alias;
import io.github.dsheirer.alias.action.AliasAction;
import io.github.dsheirer.alias.action.AliasActionType;
import io.github.dsheirer.alias.action.beep.BeepAction;
import io.github.dsheirer.alias.action.clip.ClipAction;
import io.github.dsheirer.alias.action.script.ScriptAction;
import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.AliasIDType;
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.legacy.fleetsync.FleetsyncID;
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
import io.github.dsheirer.alias.id.legacy.mobileID.Min;
import io.github.dsheirer.alias.id.legacy.mpt1327.MPT1327ID;
import io.github.dsheirer.alias.id.legacy.nonrecordable.NonRecordable;
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AliasFactory
{
@@ -64,6 +58,11 @@ public class AliasFactory
BroadcastChannel copyBroadcast = new BroadcastChannel();
copyBroadcast.setChannelName(originalBroadcast.getChannelName());
return copyBroadcast;
case DCS:
Dcs originalDcs = (Dcs)id;
Dcs copyDcs = new Dcs();
copyDcs.setDCSCode(originalDcs.getDCSCode());
return copyDcs;
case ESN:
Esn originalESN = (Esn)id;
Esn copyESN = new Esn();
@@ -80,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();
@@ -187,6 +198,7 @@ public class AliasFactory
copy.setGroup(original.getGroup());
copy.setColor(original.getColor());
copy.setIconName(original.getIconName());
copy.setRecordable(original.isRecordable());
return copy;
}
@@ -234,65 +246,4 @@ public class AliasFactory
return copy;
}
public static AliasID getAliasID(AliasIDType type)
{
switch(type)
{
case BROADCAST_CHANNEL:
return new BroadcastChannel();
case ESN:
return new Esn();
case FLEETSYNC:
return new FleetsyncID();
case LTR_NET_UID:
return new UniqueID();
case LOJACK:
return new LoJackFunctionAndID();
case MDC1200:
return new MDC1200ID();
case MIN:
return new Min();
case MPT1327:
return new MPT1327ID();
case NON_RECORDABLE:
return new NonRecordable();
case PRIORITY:
return new Priority();
case RADIO_ID:
return new Radio();
case RADIO_ID_RANGE:
return new RadioRange();
case RECORD:
return new Record();
case TALKGROUP:
return new Talkgroup();
case TALKGROUP_RANGE:
return new TalkgroupRange();
case SITE:
return new SiteID();
case STATUS:
return new UserStatusID();
case LEGACY_TALKGROUP:
return new LegacyTalkgroupID();
default:
throw new IllegalArgumentException("Unrecognized Alias ID type: " + type);
}
}
public static AliasAction getAliasAction(AliasActionType type)
{
switch(type)
{
case BEEP:
return new BeepAction();
case CLIP:
return new ClipAction();
case SCRIPT:
return new ScriptAction();
default:
throw new IllegalArgumentException("Unrecognized Alias Action type: " + type);
}
}
}

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,32 +21,34 @@ package io.github.dsheirer.alias;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasID;
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;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
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;
import io.github.dsheirer.module.decode.dcs.DCSCode;
import io.github.dsheirer.protocol.Protocol;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -57,6 +59,10 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* List of aliases that share the same alias list name and provides convenient methods for looking up alias
@@ -67,6 +73,7 @@ public class AliasList
private final static Logger mLog = LoggerFactory.getLogger(AliasList.class);
private Map<Protocol,TalkgroupAliasList> mTalkgroupProtocolMap = new EnumMap<>(Protocol.class);
private Map<Protocol,RadioAliasList> mRadioProtocolMap = new EnumMap<>(Protocol.class);
private Map<DCSCode,Alias> mDCSCodeAliasMap = new EnumMap<>(DCSCode.class);
private Map<String,Alias> mESNMap = new HashMap<>();
private Map<Integer,Alias> mUnitStatusMap = new HashMap<>();
private Map<Integer,Alias> mUserStatusMap = new HashMap<>();
@@ -152,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;
@@ -178,6 +211,12 @@ public class AliasList
radioRangeAliasList.add(radioRange, alias);
break;
case DCS:
if(id instanceof Dcs dcs)
{
mDCSCodeAliasMap.put(dcs.getDCSCode(), alias);
}
break;
case ESN:
String esn = ((Esn)id).getEsn();
@@ -379,6 +418,8 @@ public class AliasList
}
break;
case PATCH_GROUP:
List<Alias> aliases = new ArrayList<>();
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
PatchGroup patchGroup = patchGroupIdentifier.getValue();
@@ -386,8 +427,6 @@ public class AliasList
if(patchGroupAliasList != null)
{
List<Alias> aliases = new ArrayList<>();
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
if(alias != null)
@@ -395,19 +434,36 @@ public class AliasList
aliases.add(alias);
}
for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedGroupIdentifiers())
for(TalkgroupIdentifier patchedTalkgroup: patchGroup.getPatchedTalkgroupIdentifiers())
{
Alias patchedAlias = patchGroupAliasList.getAlias(patchedGroup);
Alias patchedTalkgroupAlias = patchGroupAliasList.getAlias(patchedTalkgroup);
if(patchedAlias != null && !aliases.contains(patchedAlias))
if(patchedTalkgroupAlias != null && !aliases.contains(patchedTalkgroupAlias))
{
aliases.add(patchedAlias);
aliases.add(patchedTalkgroupAlias);
}
}
return aliases;
}
break;
if(patchGroup.hasPatchedRadios())
{
RadioAliasList radioAliasList = mRadioProtocolMap.get(patchGroupIdentifier.getProtocol());
if(radioAliasList != null)
{
for(RadioIdentifier patchedRadio: patchGroup.getPatchedRadioIdentifiers())
{
Alias patchedRadioAlias = radioAliasList.getAlias(patchedRadio);
if(patchedRadioAlias != null && !aliases.contains(patchedRadioAlias))
{
aliases.add(patchedRadioAlias);
}
}
}
}
return aliases;
case RADIO:
RadioIdentifier radio = (RadioIdentifier)identifier;
@@ -439,9 +495,9 @@ public class AliasList
}
break;
case TONE:
if(identifier instanceof ToneIdentifier)
if(identifier instanceof ToneIdentifier toneIdentifier)
{
ToneSequence toneSequence = ((ToneIdentifier)identifier).getValue();
ToneSequence toneSequence = toneIdentifier.getValue();
if(toneSequence != null && toneSequence.hasTones())
{
@@ -454,6 +510,15 @@ public class AliasList
}
}
}
else if(identifier instanceof DCSIdentifier dcsIdentifier)
{
DCSCode dcsCode = dcsIdentifier.getValue();
if(dcsCode != null)
{
return toList(mDCSCodeAliasMap.get(dcsCode));
}
}
break;
}
}
@@ -465,9 +530,7 @@ public class AliasList
{
if(alias != null)
{
List<Alias> aliases = new ArrayList<>();
aliases.add(alias);
return aliases;
return Collections.singletonList(alias);
}
return Collections.emptyList();
@@ -590,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<>();
@@ -599,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);
@@ -620,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)
@@ -672,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<>();
@@ -681,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);
@@ -702,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

@@ -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
@@ -23,17 +23,16 @@ import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Alias Model contains all aliases and is responsible for creation and management of alias lists. Alias lists are a
@@ -184,10 +183,8 @@ public class AliasModel
*/
public void addAliases(List<Alias> aliases)
{
for(Alias alias : aliases)
{
addAlias(alias);
}
removeAliases(aliases);
mAliases.addAll(aliases);
}
/**
@@ -241,6 +238,26 @@ public class AliasModel
}
}
/**
* Removes the list of aliases from this model and any alias lists that might contain each alias.
* @param aliases
*/
public void removeAliases(List<Alias> aliases)
{
if(aliases != null && !aliases.isEmpty())
{
mAliases.removeAll(aliases);
for(Alias alias: aliases)
{
if(hasAliasList(alias.getAliasListName()))
{
getAliasList(alias.getAliasListName()).removeAlias(alias);
}
}
}
}
/**
* Retrieves all aliases that match the alias list and have at least one alias ID of the specified type
* @param aliasListName to search

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id;
@@ -26,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
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.legacy.fleetsync.FleetsyncID;
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
@@ -37,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;
@@ -54,6 +54,7 @@ import javafx.util.Callback;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = BroadcastChannel.class, name = "broadcastChannel"),
@JsonSubTypes.Type(value = Dcs.class, name = "dcs"),
@JsonSubTypes.Type(value = Esn.class, name = "esn"),
@JsonSubTypes.Type(value = FleetsyncID.class, name = "fleetsyncID"),
@JsonSubTypes.Type(value = LegacyTalkgroupID.class, name = "talkgroupID"),
@@ -62,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

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id;
@@ -26,11 +23,14 @@ import java.util.EnumSet;
public enum AliasIDType
{
BROADCAST_CHANNEL("Audio Broadcast Channel"),
DCS("Digital Coded Squelch (DCS)"),
ESN("ESN"),
INVERT("Audio Inversion"),
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"),
@@ -58,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,111 @@
/*
* *****************************************************************************
* 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.dcs;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.module.decode.dcs.DCSCode;
/**
* Digital Coded Squelch (DCS) tone identifier
*/
public class Dcs extends AliasID implements Comparable<Dcs>
{
private DCSCode mDCSCode;
@Override
public AliasIDType getType()
{
return AliasIDType.DCS;
}
@Override
public boolean matches(AliasID id)
{
if(isValid() && id instanceof Dcs other)
{
return other.isValid() && getDCSCode().equals(other.getDCSCode());
}
return false;
}
/**
* DCS code
* @return DCS code or null.
*/
@JacksonXmlProperty(isAttribute = true, localName = "code")
public DCSCode getDCSCode()
{
return mDCSCode;
}
/**
* Sets the DCS code value.
* @param dcsCode to set
*/
public void setDCSCode(DCSCode dcsCode)
{
mDCSCode = dcsCode;
updateValueProperty();
}
@Override
public boolean isValid()
{
return mDCSCode != null;
}
@Override
public boolean isAudioIdentifier()
{
return false;
}
@Override
public String toString()
{
if(isValid())
{
return getDCSCode().toString();
}
return "DCS-Invalid - No Tone Selected";
}
@Override
public int compareTo(Dcs o)
{
if(isValid())
{
if(o.isValid())
{
return getDCSCode().compareTo(o.getDCSCode());
}
else
{
return 1;
}
}
return -1;
}
}

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

@@ -34,6 +34,8 @@ import io.github.dsheirer.protocol.Protocol;
*/
public enum TalkgroupFormat
{
AM("*****", 1, 0xFFFF, "1 to 65,535",
"AM valid value range is 1-65,535"),
APCO25("********", 0, 0xFFFF, "0 to 65,535",
"<html>APCO25 talkgroup valid range is 0 to 65,535"),
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
@@ -122,6 +124,8 @@ public enum TalkgroupFormat
switch(protocol)
{
case AM:
return AM;
case APCO25:
return APCO25;
case FLEETSYNC:

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.alias.id.talkgroup;
@@ -23,21 +22,20 @@ package io.github.dsheirer.alias.id.talkgroup;
import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AnalogTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.NBFMTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.EnumMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Protocol-specific formatting of talkgroup values and parsing of formatted talkgroup values.
@@ -49,6 +47,10 @@ public class TalkgroupFormatter
static
{
AnalogTalkgroupFormatter analogTalkgroupFormatter = new AnalogTalkgroupFormatter();
mFormatterMap.put(Protocol.AM, analogTalkgroupFormatter);
mFormatterMap.put(Protocol.NBFM, analogTalkgroupFormatter);
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
mFormatterMap.put(Protocol.FLEETSYNC, new FleetsyncTalkgroupFormatter());
@@ -57,7 +59,6 @@ public class TalkgroupFormatter
mFormatterMap.put(Protocol.LTR_NET, ltr);
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
mFormatterMap.put(Protocol.NBFM, new NBFMTalkgroupFormatter());
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}

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,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;
@@ -106,7 +105,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
* Gets the current audio segment, or creates a new audio segment as necessary and broadcasts it to any registered
* listener(s).
*/
protected AudioSegment getAudioSegment()
public AudioSegment getAudioSegment()
{
synchronized(this)
{
@@ -135,7 +134,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
}
}
protected void addAudio(float[] audioBuffer)
public void addAudio(float[] audioBuffer)
{
AudioSegment audioSegment = getAudioSegment();

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
@@ -43,6 +43,7 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
{
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
private static float[] sHighPassFilterCoefficients;
private final boolean mAudioFilterEnable;
static
{
@@ -70,27 +71,33 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
}
}
private IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
/**
* Creates an Audio Module.
*
* @param aliasList for aliasing identifiers
* @param timeslot for this audio module
* @param maxAudioSegmentLength in milliseconds
* @param audioFilterEnable to enable or disable high-pass audio filter
*/
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength)
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, boolean audioFilterEnable)
{
super(aliasList, timeslot, maxAudioSegmentLength);
mAudioFilterEnable = audioFilterEnable;
}
/**
* Creates an Audio Module.
* @param aliasList for aliasing identifiers
* @param audioFilterEnable to enable or disable high-pass audio filter
*/
public AudioModule(AliasList aliasList)
public AudioModule(AliasList aliasList, boolean audioFilterEnable)
{
super(aliasList);
mAudioFilterEnable = audioFilterEnable;
}
@Override
@@ -121,7 +128,11 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
{
if(mSquelchState == SquelchState.UNSQUELCH)
{
audioBuffer = mHighPassFilter.filter(audioBuffer);
if(mAudioFilterEnable)
{
audioBuffer = mHighPassFilter.filter(audioBuffer);
}
addAudio(audioBuffer);
}
}
@@ -141,11 +152,16 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
@Override
public void receive(SquelchStateEvent event)
{
mSquelchState = event.getSquelchState();
SquelchState squelchState = event.getSquelchState();
if(mSquelchState == SquelchState.SQUELCH)
if(mSquelchState != squelchState)
{
closeAudioSegment();
mSquelchState = squelchState;
if(mSquelchState == SquelchState.SQUELCH)
{
closeAudioSegment();
}
}
}
}

View File

@@ -1,18 +1,21 @@
/*******************************************************************************
* sdr-trunk
* 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 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.audio;
@@ -26,15 +29,6 @@ import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -43,6 +37,14 @@ 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;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio segment containing all related metadata and a dynamic collection of audio packets. An audio segment can be
@@ -138,6 +140,15 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mComplete;
}
/**
* Indicates if this audio segment is completed.
* @return true if completed.
*/
public boolean isComplete()
{
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.

View File

@@ -24,6 +24,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
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;
@@ -46,6 +48,8 @@ import java.net.SocketAddress;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@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

@@ -22,6 +22,12 @@ 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.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzFeedConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
@@ -61,6 +67,12 @@ public class BroadcastFactory
case BROADCASTIFY_CALL:
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case RDIOSCANNER_CALL:
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case OPENMHZ:
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case BROADCASTIFY:
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
inputAudioFormat, mp3Setting, aliasModel);
@@ -99,8 +111,12 @@ public class BroadcastFactory
{
case BROADCASTIFY_CALL:
return new BroadcastifyCallConfiguration(format);
case RDIOSCANNER_CALL:
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

@@ -31,6 +31,8 @@ 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"),
OPENMHZ("OpenMHz", "images/openmhz.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),

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
@@ -38,9 +38,6 @@ 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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
@@ -48,12 +45,13 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
@@ -475,7 +473,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
StringBuilder sb = new StringBuilder();
sb.append(patchGroup.getPatchGroup().getValue().toString());
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
{
sb.append(",").append(patched.getValue());
}
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
{
sb.append(",").append(patched.getValue());
}

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,634 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.openmhz;
import com.google.common.net.HttpHeaders;
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.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;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
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
private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
* @param config to use
* @param aliasModel for access to aliases
*/
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(config);
mAliasModel = aliasModel;
}
/**
* Starts the audio recording processor thread
*/
@Override
public void start()
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
/**
* OpenMHz API does not currently expose a test method.
* TODO: FIX THIS
*/
if(response == "OK")// && response.toLowerCase().startsWith("<head><title>502 bad gateway</title></head>"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
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.
*
* OpenMHz does not have a test API endpoint, so we look for the incomplete call response.
*/
private boolean connected()
{
if(getBroadcastState() != BroadcastState.CONNECTED &&
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
if(response != null && response == "200")
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
setBroadcastState(BroadcastState.ERROR);
}
}
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);
String responseBody = response.body();
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)
{
return e.getLocalizedMessage();
}
}
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,233 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.streaming;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.controlsfx.control.ToggleSwitch;
/**
* 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
* @param playlistManager for accessing the broadcast model
*/
public OpenMHzEditor(PlaylistManager playlistManager)
{
super(playlistManager);
}
@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

@@ -0,0 +1,56 @@
/*
* *****************************************************************************
* 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.rdioscanner;
/**
* HTTP headers used for posting to Rdio Scanner API
*/
public enum FormField
{
AUDIO("audio"),
AUDIO_NAME("audioName"),
AUDIO_TYPE("audioType"),
DATE_TIME("dateTime"),
FREQUENCIES("frequencies"),
FREQUENCY("frequency"),
KEY("key"),
PATCHES("patches"),
SOURCE("source"),
SOURCES("sources"),
SYSTEM("system"),
SYSTEM_LABEL("systemLabel"),
TALKGROUP_ID("talkgroup"),
TALKGROUP_GROUP("talkgroupGroup"),
TALKGROUP_LABEL("talkgroupLabel"),
TALKGROUP_TAG("talkgroupTag"),
TEST("test");
private String mHeader;
FormField(String header)
{
mHeader = header;
}
public String getHeader()
{
return mHeader;
}
}

View File

@@ -0,0 +1,618 @@
/*
* *****************************************************************************
* 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.rdioscanner;
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.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;
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 java.io.IOException;
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio broadcaster to push completed audio recordings to the Rdio Scanner call upload API.
*
*/
public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScannerConfiguration>
{
private final static Logger mLog = LoggerFactory.getLogger(RdioScannerBroadcaster.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 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
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)
{
super(config);
mAliasModel = aliasModel;
}
/**
* Starts the audio recording processor thread
*/
@Override
public void start()
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
/**
* Rdio Scanner API does not currently expose a test method.
*/
if(response != null && response.toLowerCase().startsWith("incomplete call data: no talkgroup"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
mLog.error("Error connecting to Rdio Scanner 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.
*
* Rdio Scanner does not have a test API endpoint, so we look for the incomplete call response.
*/
private boolean connected()
{
if(getBroadcastState() != BroadcastState.CONNECTED &&
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
{
setBroadcastState(BroadcastState.CONNECTING);
String response = testConnection(getBroadcastConfiguration());
mLastConnectionAttempt = System.currentTimeMillis();
if(response != null && response.toLowerCase().startsWith("incomplete call data: no talkgroup"))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
setBroadcastState(BroadcastState.ERROR);
}
}
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)
{
float durationSeconds = (float)(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);
String path = audioRecording.getPath().toString();
// Remove TEMPORARY_STREAM_FILE_SUFFIX
String audioName = path.substring(path.substring(0, path.lastIndexOf("_")).lastIndexOf("_") + 1);
try
{
byte[] audioBytes = null;
try
{
audioBytes = Files.readAllBytes(audioRecording.getPath());
}
catch(IOException e)
{
mLog.error("Rdio Scanner API - audio recording file not found - ignoring upload");
}
if(audioBytes != null)
{
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)
.addPart(FormField.SOURCE, radioId)
.addPart(FormField.FREQUENCY, frequency)
.addPart(FormField.TALKGROUP_LABEL, talkgroupLabel)
.addPart(FormField.TALKGROUP_GROUP, talkgroupGroup)
.addPart(FormField.SYSTEM_LABEL, systemLabel)
.addPart(FormField.PATCHES, patches);
HttpRequest fileRequest = HttpRequest.newBuilder()
.uri(URI.create(getBroadcastConfiguration().getHost()))
.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("Rdio Scanner API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("Rdio Scanner API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
incrementErrorAudioCount();
broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
}
else
{
String fileResponseString = fileResponse.body();
if(fileResponseString.contains("Call imported successfully."))
{
incrementStreamedAudioCount();
broadcast(new BroadcastEvent(RdioScannerBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
else if(fileResponseString.contains("duplicate call rejected"))
{
//Rdio Scanner is telling us to skip audio upload - someone already uploaded it
audioRecording.removePendingReplay();
}
else
{
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
mLog.error("Rdio Scanner API file upload fail [" +
fileResponse.statusCode() + "] response [" +
fileResponse.body() + "]");
}
}
});
}
else
{
//Register an error for the file not found exception
mLog.error("Rdio Scanner API - upload file not found [" +
audioRecording.getPath().toString() + "]");
incrementErrorAudioCount();
broadcast(new BroadcastEvent(RdioScannerBroadcaster.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 RdioScanner Call API service
* @param configuration containing API key and system id
* @return error string or null if test is successful
*/
public static String testConnection(RdioScannerConfiguration configuration)
{
HttpClient httpClient = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
bodyBuilder.addPart(FormField.KEY, configuration.getApiKey())
.addPart(FormField.SYSTEM, configuration.getSystemID())
.addPart(FormField.TEST, 1);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(configuration.getHost()))
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
.header(HttpHeaders.ACCEPT, "*/*")
.POST(bodyBuilder.build())
.build();
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
try
{
HttpResponse<String> response = httpClient.send(request, responseHandler);
String responseBody = response.body();
return (responseBody != null ? responseBody : "(no response)") + " Status Code:" + response.statusCode();
}
catch(Exception e)
{
return e.getLocalizedMessage();
}
}
public class AudioRecordingProcessor implements Runnable
{
@Override
public void run()
{
processRecordingQueue();
}
}
public static void main(String[] args)
{
mLog.debug("Starting ...");
RdioScannerConfiguration config = new RdioScannerConfiguration();
config.setHost("https://api.RdioScanner.com/call-upload-dev");
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
config.setSystemID(11);
String response = testConnection(config);
if(response == null)
{
mLog.debug("Test Successful!");
}
else
{
if(response.contains("1 Invalid-API-Key"))
{
mLog.error("Invalid API Key");
}
else if(response.contains("1 API-Key-Access-Denied"))
{
mLog.error("System ID not valid for API Key");
}
else
{
mLog.debug("Response: " + response);
}
}
mLog.debug("Finished!");
}
}

View File

@@ -0,0 +1,227 @@
/*
* *****************************************************************************
* 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.rdioscanner;
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 RdioScanner call event
*/
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 String audioName = null;
/**
* Constructs an instance
*/
public RdioScannerBuilder()
{
}
/**
* Access the static multi-part boundary string
*/
public String getBoundary()
{
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
*/
public RdioScannerBuilder addFile(byte[] value)
{
audioBytes = value;
return this;
}
/**
* Adds a string part to the call
*/
public RdioScannerBuilder 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 RdioScannerBuilder 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; filename=\"").append(audioName);
sb.append("\"; name=\"").append("audio").append("\"\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,135 @@
/*
* *****************************************************************************
* 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.rdioscanner;
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.IntegerProperty;
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 RdioScannerConfiguration extends BroadcastConfiguration
{
private IntegerProperty mSystemID = new SimpleIntegerProperty();
private StringProperty mApiKey = new SimpleStringProperty();
/**
* Constructor for faster jackson
*/
public RdioScannerConfiguration()
{
this(BroadcastFormat.MP3);
}
/**
* Public constructor.
* @param format to use for audio recording (MP3)
*/
public RdioScannerConfiguration(BroadcastFormat format)
{
super(format);
//The parent class binds this property, so we unbind it and rebind it here
mValid.unbind();
mValid.bind(Bindings.and(Bindings.and(Bindings.greaterThan(mSystemID, 0), Bindings.isNotNull(mApiKey)),
Bindings.isNotNull(mHost)));
if(mHost.getValue() == null || mHost.getValue().isEmpty())
{
mHost.set("http://localhost");
}
}
/**
* System ID as a property
*/
public IntegerProperty systemIDProperty()
{
return mSystemID;
}
/**
* 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 ID as provided by RdioScanner.com
*/
@JacksonXmlProperty(isAttribute = true, localName = "system_id")
public int getSystemID()
{
return mSystemID.get();
}
/**
* Sets the system ID provided by RdioScanner.com
*/
public void setSystemID(int systemID)
{
mSystemID.set(systemID);
}
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
@Override
public BroadcastServerType getBroadcastServerType()
{
return BroadcastServerType.RDIOSCANNER_CALL;
}
@Override
public BroadcastConfiguration copyOf()
{
RdioScannerConfiguration copy = new RdioScannerConfiguration();
copy.setSystemID(getSystemID());
return copy;
}
}

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.rdioscanner;
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 RdioScannerFeedConfiguration extends IcecastTCPConfiguration
{
private final static Logger mLog = LoggerFactory.getLogger(RdioScannerFeedConfiguration.class);
private int mFeedID;
public RdioScannerFeedConfiguration()
{
//No-arg constructor for JAXB
this(BroadcastFormat.MP3);
}
/**
* RdioScanner configuration for Icecast 2.3.2 compatible servers
*
* @param format of output audio (MP3)
*/
public RdioScannerFeedConfiguration(BroadcastFormat format)
{
super(format);
setBitRate(16);
setChannels(1);
setSampleRate(8000);
setInline(true);
}
public static RdioScannerFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
{
RdioScannerFeedConfiguration config = new RdioScannerFeedConfiguration(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()
{
RdioScannerFeedConfiguration copy = new RdioScannerFeedConfiguration(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());
//RdioScanner 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.RDIOSCANNER_CALL;
}
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
public int getFeedID()
{
return mFeedID;
}
public void setFeedID(int feedID)
{
mFeedID = feedID;
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.codec.mbe;
@@ -32,11 +29,6 @@ import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import jmbe.iface.IAudioCodec;
import jmbe.iface.IAudioCodecLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -44,15 +36,20 @@ import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import jmbe.iface.IAudioCodec;
import jmbe.iface.IAudioCodecLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
ISquelchStateListener
{
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
private static List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private IAudioCodec mAudioCodec;
private UserPreferences mUserPreferences;
private final UserPreferences mUserPreferences;
private static Class sLoadedJmbeAudioConverterClass;
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
{
@@ -70,7 +67,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
mAudioCodec = null;
}
protected IAudioCodec getAudioCodec()
public IAudioCodec getAudioCodec()
{
return mAudioCodec;
}
@@ -110,38 +107,67 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
protected abstract String getCodecName();
/**
* Loads audio frame processing chain. Constructs an imbe targetdataline
* to receive the raw imbe frames. Adds an IMBE to 8k PCM format conversion
* stream wrapper. Finally, adds an upsampling (8k to 48k) stream wrapper.
* Loads JMBE audio converter library class and then instantiates new converter instances from the loaded class.
*/
protected void loadConverter()
{
IAudioCodec audioConverter = null;
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
if(path != null)
if(sLoadedJmbeAudioConverterClass == null)
{
try
Path path = mUserPreferences.getJmbeLibraryPreference().getPathJmbeLibrary();
if(path != null)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("Loading JMBE library from [" + path.toString() + "]");
mLog.info("Loading JMBE library from [" + path + "]");
}
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
this.getClass().getClassLoader());
Class classToLoad = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
Object instance = classToLoad.getDeclaredConstructor().newInstance();
if(instance instanceof IAudioCodecLibrary)
try
{
IAudioCodecLibrary library = (IAudioCodecLibrary)instance;
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
this.getClass().getClassLoader());
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
}
catch(IllegalArgumentException iae)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
{
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
}
}
catch(MalformedURLException mue)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(ClassNotFoundException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - class not found");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
}
if(sLoadedJmbeAudioConverterClass != null)
{
try
{
Object instance = sLoadedJmbeAudioConverterClass.getDeclaredConstructor().newInstance();
if(instance instanceof IAudioCodecLibrary library)
{
if((library.getMajorVersion() == 1 && library.getMinorVersion() >= 0 &&
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
{
audioConverter = library.getAudioConverter(getCodecName());
@@ -169,38 +195,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
}
}
}
catch(IllegalArgumentException iae)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
{
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
}
}
catch(NoSuchMethodException nsme)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(MalformedURLException mue)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(ClassNotFoundException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - class not found");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(InvocationTargetException ite)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
@@ -225,6 +219,14 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(NoSuchMethodException nsme)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
else
{
@@ -235,13 +237,6 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
}
}
if(audioConverter != null)
{
mAudioCodec = audioConverter;
}
else
{
mAudioCodec = null;
}
mAudioCodec = audioConverter;
}
}

View File

@@ -1,27 +1,46 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.preference.UserPreferences;
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;
@@ -32,79 +51,130 @@ public class MBECallSequenceConverter
{
private final static Logger mLog = LoggerFactory.getLogger(MBECallSequenceConverter.class);
// public static void convert(Path input, Path output) throws IOException
// {
// InputStream inputStream = Files.newInputStream(input);
// ObjectMapper mapper = new ObjectMapper();
// MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
// convert(sequence, output);
// }
//
// public static void convert(MBECallSequence callSequence, Path outputPath)
// {
// if(callSequence == null || callSequence.isEncrypted())
// {
// throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
// }
//
// if(callSequence != null && !callSequence.isEncrypted())
// {
// ThumbDv.AudioProtocol protocol = ThumbDv.AudioProtocol.P25_PHASE2;
//
// AudioPacketWaveRecorder recorder = new AudioPacketWaveRecorder(outputPath);
// recorder.start();
//
// long delayMillis = 0;
//
// try(ThumbDv thumbDv = new ThumbDv(protocol, recorder))
// {
// thumbDv.start();
// for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
// {
// mLog.debug("Frame [" + voiceFrame.getFrame() + "] + Hex [" + AmbeResponse.toHex(voiceFrame.getFrameBytes()) + "]");
// thumbDv.decode(voiceFrame.getFrameBytes());
// delayMillis += 30;
// }
//
// if(delayMillis > 0)
// {
// delayMillis += 1000;
// try
// {
// Thread.sleep(delayMillis);
// }
// catch(InterruptedException ie)
// {
//
// }
// }
// }
// catch(IOException ioe)
// {
// mLog.error("Error", ioe);
// }
//
// recorder.stop(Paths.get(outputPath.toString().replace(".tmp", ".wav")), new WaveMetadata());
// }
// }
//
// public static void main(String[] args)
// {
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_3_TS0_65084_6591007.mbe";
// String mbe = "/home/denny/SDRTrunk/recordings/20190331085324_154250000_2_TS1_65035.mbe";
//
// Path input = Paths.get(mbe);
// Path output = Paths.get(mbe.replace(".mbe", ".tmp"));
//
// mLog.info("Converting: " + mbe);
//
// try
// {
// MBECallSequenceConverter.convert(input, output);
// }
// catch(IOException ioe)
// {
// mLog.error("Error", ioe);
// }
// }
/**
* Converts the input MBE file to PCM audio and writes to the output wave file.
* @param input path to the MBE file.
* @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
{
InputStream inputStream = Files.newInputStream(input);
ObjectMapper mapper = new ObjectMapper();
MBECallSequence sequence = mapper.readValue(inputStream, MBECallSequence.class);
convert(sequence, output);
}
public static void convert(MBECallSequence callSequence, Path outputPath)
{
if(callSequence == null || callSequence.isEncrypted())
{
throw new IllegalArgumentException("Cannot decode null or encrypted call sequence");
}
if(callSequence != null)
{
if(callSequence.getProtocol().equals(P25P1CallSequenceRecorder.PROTOCOL))
{
P25P1AudioModule audioModule = new P25P1AudioModule(new UserPreferences(), new AliasList("mbe generator"));
audioModule.setRecordAudio(true);
audioModule.start();
if(callSequence.getFromIdentifier() != null)
{
int from = 0;
try
{
from = Integer.parseInt(callSequence.getFromIdentifier());
audioModule.getIdentifierUpdateListener().receive(new IdentifierUpdateNotification(APCO25RadioIdentifier.createFrom(from),
IdentifierUpdateNotification.Operation.ADD, 0));
}
catch(Exception e)
{
mLog.error("Error parsing from identifier from value [" + callSequence.getFromIdentifier());
}
}
if(callSequence.getToIdentifier() != null)
{
int to = 0;
try
{
to = Integer.parseInt(callSequence.getToIdentifier());
audioModule.getIdentifierUpdateListener().receive(new IdentifierUpdateNotification(APCO25Talkgroup.create(to),
IdentifierUpdateNotification.Operation.ADD, 0));
}
catch(Exception e)
{
mLog.error("Error parsing from identifier from value [" + callSequence.getFromIdentifier());
}
}
IAudioCodec codec = audioModule.getAudioCodec();
for(VoiceFrame voiceFrame: callSequence.getVoiceFrames())
{
byte[] frameBytes = voiceFrame.getFrameBytes();
float[] audio = codec.getAudio(frameBytes);
audioModule.addAudio(audio);
}
AudioSegment audioSegment = audioModule.getAudioSegment();
try
{
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
}
catch(IOException ioe)
{
mLog.error("Error writing audio segment, ioe");
}
audioModule.stop();
}
}
}
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,30 +1,27 @@
/*
* *****************************************************************************
* 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.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
@@ -33,11 +30,13 @@ 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.util.ThreadPool;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import java.nio.ByteBuffer;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -51,10 +50,6 @@ import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import java.nio.ByteBuffer;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
@@ -67,7 +62,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private int mBufferStopThreshold;
private Listener<IdentifierCollection> mIdentifierCollectionListener;
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
private ScheduledFuture<?> mProcessorFuture;
private SourceDataLine mOutput;
private Mixer mMixer;
private MixerChannel mMixerChannel;
@@ -76,17 +70,18 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private boolean mCanProcessAudio = false;
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private AudioSegment mCurrentAudioSegment;
private AudioSegment mNextAudioSegment;
private ReentrantLock mLock = new ReentrantLock();
private int mCurrentBufferIndex = 0;
private UserPreferences mUserPreferences;
private BooleanProperty mEmptyProperty = new SimpleBooleanProperty(true);
private IntegerProperty mAudioPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
private ByteBuffer mAudioSegmentStartTone;
private ByteBuffer mAudioSegmentPreemptTone;
private ByteBuffer mAudioSegmentDropTone;
private boolean mRunning = false;
private ScheduledExecutorService mScheduledExecutorService;
private ScheduledFuture<?> mProcessorFuture;
private boolean mDropDuplicates;
private long mOutputLastTimestamp = 0;
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
/**
* Single audio channel playback with automatic starting and stopping of the
@@ -107,7 +102,10 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
mMixer = mixer;
mMixerChannel = mixerChannel;
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mixerChannel.name()));
mUserPreferences = userPreferences;
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
try
{
@@ -150,7 +148,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
//Run the queue processor task every 100 milliseconds or 10 times a second
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
}
@@ -167,31 +165,22 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
updateToneInsertionAudioClips();
//Register to receive directory preference update notifications so we can update the preference items
//Register to receive preference update notifications so we can update the preference items
MyEventBus.getGlobalEventBus().register(this);
}
/**
* Boolean property that indicates if this audio output has an audio segment queued or in process of playback.
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
* @return true if empty
*/
public BooleanProperty emptyProperty()
public boolean isEmpty()
{
return mEmptyProperty;
return mAudioSegmentQueue.isEmpty();
}
/**
* Audio playback priority of the current audio segment or default audio priority if nothing is currently loaded.
*/
public IntegerProperty audioPriorityProperty()
{
return mAudioPriority;
}
/**
* Schedules the audio segment for playback.
*
* Note: if a segment is currently playing and another audio segment is already queued for playback, invoking
* this method will overwrite the queued segment with the argument.
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
* calling entity.
*
* @param audioSegment to schedule for playback.
*/
@@ -199,23 +188,8 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
if(audioSegment != null)
{
mLock.lock();
if(mNextAudioSegment != null)
{
mNextAudioSegment.decrementConsumerCount();
mNextAudioSegment = null;
}
try
{
mNextAudioSegment = audioSegment;
mEmptyProperty.set(false);
}
finally
{
mLock.unlock();
}
//Audio segment use count has already been incremented by the external caller.
mAudioSegmentQueue.add(audioSegment);
}
}
@@ -254,6 +228,10 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
updateToneInsertionAudioClips();
}
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
{
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
}
}
/**
@@ -262,7 +240,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
private void updateToneInsertionAudioClips()
{
mAudioSegmentStartTone = null;
mAudioSegmentPreemptTone = null;
mAudioSegmentDropTone = null;
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
@@ -272,13 +249,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
mAudioSegmentStartTone = convert(start);
}
float[] preempt = mUserPreferences.getPlaybackPreference().getPreemptTone();
if(preempt != null)
{
mAudioSegmentPreemptTone = convert(preempt);
}
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
if(drop != null)
@@ -287,48 +257,6 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
}
/**
* Disposes of a processed audio segment and notifies the originator that processing of the segment is complete
* by decrementing the consumer count.
* @param audioSegment to dispose
*/
private void dispose(AudioSegment audioSegment)
{
mAudioPriority.unbind();
if(audioSegment != null)
{
audioSegment.decrementConsumerCount();
audioSegment.removeIdentifierUpdateNotificationListener(this);
}
}
/**
* Generates a tone indicating that a new audio segment is starting
*/
private ByteBuffer getAudioSegmentStartTone()
{
return mAudioSegmentStartTone;
}
/**
* Generates a tone indicating that the current audio segment playback has been preempted for a higher priority
* audio segment that is now starting.
*/
private ByteBuffer getAudioSegmentPreemptionTone()
{
return mAudioSegmentPreemptTone;
}
/**
* Generates a tone indicating that the current audio segment playback has been dropped because the audio segment
* has been flagged as Do Not Monitor after playback has started.
*/
private ByteBuffer getAudioSegmentDropTone()
{
return mAudioSegmentDropTone;
}
/**
* Writes the audio buffer data to the source data line. If the data line is not currently playing, write as
* much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual
@@ -355,6 +283,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
//Top off the buffer and check if we can start it
wrote += mOutput.write(buffer.array(), 0, toWrite);
mOutputLastTimestamp = System.currentTimeMillis();
checkStart();
}
@@ -363,149 +292,146 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
//This will block until the buffer is fully written to the data line
mOutput.write(buffer.array(), wrote, buffer.array().length - wrote);
mOutputLastTimestamp = System.currentTimeMillis();
}
}
}
/**
* Dispose of the currently assigned audio segment.
*/
private void disposeCurrentAudioSegment()
{
if(mCurrentAudioSegment != null)
{
mCurrentAudioSegment.decrementConsumerCount();
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
mCurrentAudioSegment = null;
broadcast(null);
}
}
/**
* Loads the next audio segment from the queue.
*/
private void loadNextAudioSegment()
{
AudioSegment audioSegment = mAudioSegmentQueue.poll();
boolean verificationInProgress = (audioSegment != null);
while(verificationInProgress)
{
if(audioSegment != null)
{
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
if(isThrowaway(audioSegment))
{
audioSegment.decrementConsumerCount();
audioSegment = mAudioSegmentQueue.poll();
}
else
{
verificationInProgress = false;
}
}
else
{
verificationInProgress = false;
}
}
mCurrentAudioSegment = audioSegment;
if(audioSegment != null)
{
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
mCurrentBufferIndex = 0;
}
/**
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
* @param audioSegment to evaluate
* @return true if the audio should be thrown away.
*/
private boolean isThrowaway(AudioSegment audioSegment)
{
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
}
/**
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
* by a threaded processor repeatedly to playback the current audio segment and check for and start a newly
* assigned audio segment. It also handles starting and stopping the playback source data line to avoid audio
* by a threaded processor repeatedly to playback the current audio segment and check for and start newly
* added audio segments. It also handles starting and stopping the playback source data line to avoid audio
* discontinuities due to buffer underruns.
*/
private void processAudio()
{
if(mNextAudioSegment != null)
if(mCurrentAudioSegment == null)
{
mLock.lock();
try
{
//Remove an assigned segment that's subsequently flagged as duplicate or do not monitor before playback
if(mNextAudioSegment.isDoNotMonitor() || (mNextAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
mNextAudioSegment = null;
if(mCurrentAudioSegment == null)
{
mEmptyProperty.set(true);
}
}
//For linked audio segments, allow the linked segment to complete first before assigning the next
else if(mNextAudioSegment.isLinked())
{
if(mCurrentAudioSegment == null)
{
mCurrentAudioSegment = mNextAudioSegment;
mNextAudioSegment = null;
mCurrentBufferIndex = 0;
if(mCurrentAudioSegment != null)
{
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
else
{
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
}
}
}
else
{
//Insert audio segment start or audio priority preemption bonk tone
if(mCurrentAudioSegment == null)
{
playAudio(getAudioSegmentStartTone());
}
else if(mCurrentBufferIndex > 0 &&
(!mCurrentAudioSegment.completeProperty().get() ||
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount()))
{
playAudio(getAudioSegmentPreemptionTone());
}
else
{
playAudio(getAudioSegmentStartTone());
}
//Close current audio segment
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = mNextAudioSegment;
mNextAudioSegment = null;
mCurrentBufferIndex = 0;
if(mCurrentAudioSegment != null)
{
mAudioPriority.bind(mCurrentAudioSegment.monitorPriorityProperty());
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
else
{
mAudioPriority.setValue(Priority.DEFAULT_PRIORITY);
}
}
}
finally
{
mLock.unlock();
}
loadNextAudioSegment();
}
if(mCurrentAudioSegment != null)
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
while(isThrowaway(mCurrentAudioSegment))
{
//Check for completed audio segment or a segment flagged as duplicate or Do Not Monitor
if(mCurrentAudioSegment.isDoNotMonitor() ||
(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()) ||
(mCurrentAudioSegment.completeProperty().get() && mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
if(mCurrentBufferIndex > 0)
{
if(mCurrentAudioSegment.isDoNotMonitor())
playAudio(mAudioSegmentDropTone);
}
disposeCurrentAudioSegment();
loadNextAudioSegment();
}
while(mCurrentAudioSegment != null && mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
{
//Continuously evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
if(isThrowaway(mCurrentAudioSegment))
{
if(mCurrentBufferIndex > 0)
{
playAudio(getAudioSegmentDropTone());
playAudio(mAudioSegmentDropTone);
}
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = null;
mLock.lock();
disposeCurrentAudioSegment();
}
else
{
if(mCurrentBufferIndex == 0)
{
playAudio(mAudioSegmentStartTone);
}
try
{
if(mNextAudioSegment == null)
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
if(audioBuffer != null)
{
mEmptyProperty.set(true);
ByteBuffer audio = convert(audioBuffer);
//This call blocks until all audio bytes are dumped into the data line.
playAudio(audio);
}
}
finally
catch(Exception e)
{
mLock.unlock();
}
return;
}
//Process any new buffers that have been added to the audio segment. If a next audio segment gets assigned
//while processing, exit the loop so that we can evaluate the next for higher priority preempt. If the next
//segment is a linked segment, ignore it so that we can close out the current segment.
while(mCurrentAudioSegment != null && (mNextAudioSegment == null || mNextAudioSegment.isLinked()) &&
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount() &&
!mCurrentAudioSegment.isDoNotMonitor() && !(mCurrentAudioSegment.isDuplicate() &&
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
if(audioBuffer != null)
{
ByteBuffer audio = convert(audioBuffer);
playAudio(audio);
mLog.error("Error while processing audio for [" + mMixerChannel.name() + "]", e);
}
}
}
//Check for completed and fully-played audio segment to closeout
if(mCurrentAudioSegment != null &&
mCurrentAudioSegment.isComplete() &&
(mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
{
disposeCurrentAudioSegment();
}
checkStop();
}
@@ -523,25 +449,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
mProcessorFuture = null;
mLock.lock();
try
{
if(mNextAudioSegment != null)
{
mNextAudioSegment.decrementConsumerCount();
mNextAudioSegment = null;
}
}
finally
{
mLock.unlock();
}
dispose(mCurrentAudioSegment);
mCurrentAudioSegment = null;
disposeCurrentAudioSegment();
mAudioEventBroadcaster.clear();
mIdentifierCollectionListener = null;
@@ -650,14 +558,28 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(mRunning)
{
//If the output buffer falls below the threshold then drain the output and stop playback
if(mOutput.isRunning() && mOutput.available() >= mBufferStopThreshold)
if(mOutput.isRunning())
{
mOutput.drain();
mOutput.stop();
mRunning = false;
if(mOutput.available() >= mBufferStopThreshold)
{
mOutput.drain();
mOutput.stop();
mRunning = false;
}
//Detect when we have a partially filled output that will neither start nor stop and the last write
//timestamp was more than the staleness threshold (500 ms) -- stop the output and discard stale contents
else if(mOutput.available() < mBufferStopThreshold &&
mOutput.available() > mBufferStartThreshold &&
(System.currentTimeMillis() - mOutputLastTimestamp >= STALE_PLAYBACK_THRESHOLD_MS))
{
mOutput.stop();
//Discard the stale buffer contents.
mOutput.flush();
mRunning = false;
}
}
//If output playback stopped on its own because the buffer emptied, then cleanup
else if(!mOutput.isRunning())
else
{
mRunning = false;
}
@@ -735,16 +657,23 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public class AudioSegmentProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
try
if(mProcessing.compareAndSet(false, true))
{
processAudio();
}
catch(Throwable t)
{
mLog.error("Error while processing audio buffers", t);
try
{
processAudio();
}
catch(Throwable t)
{
mLog.error("Error while processing audio buffers", t);
}
mProcessing.set(false);
}
}
}

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,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,6 +23,7 @@ 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.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
@@ -31,17 +31,18 @@ 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 io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages scheduling and playback of audio segments to the local users audio system.
@@ -50,8 +51,6 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
public static final String AUDIO_CHANNELS_PROPERTY = "audio.manager.channels";
public static final String AUDIO_MIXER_PROPERTY = "audio.manager.mixer";
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
@@ -64,6 +63,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
private ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
/**
* Constructs an instance.
@@ -202,29 +203,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
//Sort audio segments by playback priority and assign to empty audio outputs
if(!mAudioSegments.isEmpty())
{
//TODO: change this to take audio segment start time into account also
mAudioSegments.sort(Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
{
if(audioOutput.emptyProperty().get())
{
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
{
return;
}
}
}
}
//Preempt ongoing audio segment playback when higher priority audio segments are available
if(!mAudioSegments.isEmpty())
{
for(AudioOutput audioOutput: mAudioOutputs)
{
if(mAudioSegments.get(0).monitorPriorityProperty().get() < audioOutput.audioPriorityProperty().get())
if(audioOutput.isEmpty())
{
audioOutput.play(mAudioSegments.remove(0));
@@ -284,7 +269,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
catch(AudioException ae)
{
mLog.error("Error changing audio output to [" + configuration.toString() + "]", ae);
mLog.error("Error changing audio output to [" + configuration + "]", ae);
}
}
}
@@ -332,7 +317,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
}
mProcessingTask = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
mMixerChannelConfiguration = entry;
@@ -343,7 +328,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
* Current audio playback mixer channel configuration setting.
*/
@Override
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException
public MixerChannelConfiguration getMixerChannelConfiguration()
{
return mMixerChannelConfiguration;
}
@@ -384,16 +369,23 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
*/
public class AudioSegmentProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
{
try
if(mProcessing.compareAndSet(false, true))
{
processAudioSegments();
}
catch(Throwable t)
{
mLog.error("Encountered error while processing audio segments", t);
try
{
processAudioSegments();
}
catch(Throwable t)
{
mLog.error("Encountered error while processing audio segments", t);
}
mProcessing.set(false);
}
}
}

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;
@@ -33,6 +34,8 @@ public class BinaryMessage extends BitSet
private static final int[] CHARACTER_7_BIT = new int[]{0, 1, 2, 3, 4, 5, 6};
private static final int[] CHARACTER_8_BIT = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
private static final String UTF_8 = "UTF-8";
private static final String GB2312 = "GB2312";
/**
* Logical (ie constructed) size of this bitset, despite the actual size of
@@ -274,8 +277,7 @@ public class BinaryMessage extends BitSet
}
else
{
throw new BitSetFullException("bitset is full -- contains " +
(mPointer + 1) + "bits");
throw new BitSetFullException("bitset is full -- contains [" + mPointer + "/" + size() + "] bits");
}
}
@@ -843,13 +845,18 @@ public class BinaryMessage extends BitSet
*/
public int getTwosComplement(int start, int end)
{
BinaryMessage sub = getSubMessage(start, end);
boolean negative = sub.get(start);
sub.flip(0, sub.size());
int value = sub.getInt(0, sub.size()) + 1;
value *= (negative ? -1 : 1);
return value;
if(get(start))
{
//Negative value - flip and add one
BinaryMessage fragment = getSubMessage(start, end);
fragment.flip(0, fragment.size());
return fragment.getInt(1, fragment.size()) + 1;
}
else
{
//Positive value - return the contents.
return getInt(start + 1, end);
}
}
@@ -1236,6 +1243,26 @@ public class BinaryMessage extends BitSet
return new String(bytes, UTF_8);
}
/**
* Parse GB2312 Simplified Chinese Characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
* @throws UnsupportedEncodingException if GB2312 encoding is not supported.
*/
public String parseGB2312(int offset, int characterCount) throws UnsupportedEncodingException
{
int length = characterCount * 2;
byte[] bytes = new byte[length];
for(int x = 0; x < length; x++)
{
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes, GB2312);
}
/**
* Parse 16-bit unicode characters from the message starting at the offset.
* @param offset where to start parsing.
@@ -1252,7 +1279,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

@@ -132,12 +132,10 @@ public enum SyncPattern
true, true, false, false //1100 0xC
}),
/* Sync (0x158) = 1101011000 */
/* Sync (0x158) = 101011000 */
PASSPORT( new boolean[]
{
true, //0001 0x1
false, true, false, true, //0101 0x5
true, false, false, false //1000 0x8
true,false,true,false,true,true,false,false,false
} ),
LTR_STANDARD_OSW( new boolean[]

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
@@ -84,7 +84,7 @@ public class DcCorrectionManager
{
mDcCalculationsRemaining--;
}
//
// mLog.info("DC: " + mDecimalFormat.format(mAverageDc) +
// " RES:" + mDecimalFormat.format(residualOffsetToRemove) +
// " REM:" + mDcCalculationsRemaining);

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,21 +21,16 @@ package io.github.dsheirer.buffer;
import java.util.Arrays;
/**
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples. Initially
* fills buffer with 0-valued samples
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
* Circular sample buffer - allocates a buffer and stores samples in a circular fashion, overwriting older samples with
* newly arrived samples. Initially fills buffer with 0-valued (default) samples or with a specified value.
*/
public class FloatCircularBuffer
{
private float[] mBuffer;
private int mBufferPointer = 0;
private float mOldestSample;
/**
* Creates a circular double buffer of the specified size and all entries filled with the specified initial value.
*
* @param size of the buffer
* @param initialFillValue to fill the buffer entries
*/
@@ -47,7 +42,6 @@ public class FloatCircularBuffer
/**
* Creates a circular double buffer of the specified size and all entries filled with an initial value of zero.
*
* @param size of the buffer
*/
public FloatCircularBuffer(int size)
@@ -55,33 +49,34 @@ public class FloatCircularBuffer
this(size, 0.0f);
}
public float[] getBuffer()
/**
* Resets the delay buffer to an all zeros state
*/
public void reset(float fillValue)
{
return mBuffer;
Arrays.fill(mBuffer, fillValue);
}
/**
* Gets the current buffer value and overwrites that value position in the buffer with the new value.
* Puts the new value into the buffer and returns the oldest buffer value that it replaced
*
* @param newValue to add to the buffer
* @return oldest value that was overwritten by the new value
*/
public float getAndPut(float newValue)
public float get(float newValue)
{
mOldestSample = mBuffer[mBufferPointer];
put(newValue);
return mOldestSample;
}
float oldestSample = mBuffer[mBufferPointer];
/**
* Puts the value into the buffer, updates the pointer and returns the buffer value at the pointer position.
* @param sample to add
* @return next pointed sample.
*/
public float putAndGet(float sample)
{
put(sample);
return mBuffer[mBufferPointer];
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
return oldestSample;
}
public float[] getAll()
@@ -105,13 +100,70 @@ public class FloatCircularBuffer
/**
* Places the new value into the buffer, overwriting the oldest value
*
* @param newValue to add to the buffer
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
mBufferPointer %= mBuffer.length;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the maximum from the values currently in the buffer
*/
public float max(float referenceValue)
{
float max = referenceValue;
for(int x = 0; x < mBuffer.length; x++)
{
if(mBuffer[x] > max)
{
max = mBuffer[x];
}
}
// System.out.println("Max [" + max + "] from " + Arrays.toString(mBuffer));
return max;
}
/**
* Returns the minimum from the values currently in the buffer
*/
public float min()
{
float min = 0.0f;
for(int x = 0; x < mBuffer.length; x++)
{
if(mBuffer[x] < min)
{
min = mBuffer[x];
}
}
return min;
}
/**
* Calculates the average/mean of the values contained in this buffer
*/
public float mean()
{
double accumulator = 0.0;
for(float value: mBuffer)
{
accumulator += value;
}
return (float)(accumulator / mBuffer.length);
}
}

View File

@@ -22,6 +22,8 @@ package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Arrays;
import java.util.Iterator;
/**
@@ -30,6 +32,7 @@ import java.util.Iterator;
public class FloatNativeBuffer extends AbstractNativeBuffer
{
private float[] mInterleavedComplexSamples;
private static final int BUFFER_SIZE = 2048;
/**
* Constructs an instance
@@ -81,48 +84,51 @@ public class FloatNativeBuffer extends AbstractNativeBuffer
private class ComplexSamplesIterator implements Iterator<ComplexSamples>
{
private boolean mEmpty;
private int mBufferPointer = 0;
@Override
public boolean hasNext()
{
return !mEmpty;
return mBufferPointer < mInterleavedComplexSamples.length;
}
@Override
public ComplexSamples next()
{
if(mEmpty)
if(!hasNext())
{
throw new IllegalStateException("No more samples");
}
mEmpty = true;
return SampleUtils.deinterleave(mInterleavedComplexSamples, getTimestamp());
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
mBufferPointer += BUFFER_SIZE * 2;
return SampleUtils.deinterleave(chunk, getTimestamp());
}
}
private class InterleavedComplexSamplesIterator implements Iterator<InterleavedComplexSamples>
{
private boolean mEmpty;
private int mBufferPointer = 0;
@Override
public boolean hasNext()
{
return !mEmpty;
return mBufferPointer < mInterleavedComplexSamples.length;
}
@Override
public InterleavedComplexSamples next()
{
if(mEmpty)
if(!hasNext())
{
throw new IllegalStateException("No more samples");
}
mEmpty = true;
return new InterleavedComplexSamples(mInterleavedComplexSamples, getTimestamp());
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
mBufferPointer += BUFFER_SIZE * 2;
return new InterleavedComplexSamples(chunk, getTimestamp());
}
}
}

View File

@@ -1,124 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer;
/**
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples.
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/
public class RealCircularBuffer
{
float[] mBuffer;
int mBufferPointer = 0;
public RealCircularBuffer(int size)
{
mBuffer = new float[size];
}
public int getSize()
{
return mBuffer.length;
}
public float[] getBuffer()
{
return mBuffer;
}
/**
* Puts the new value into the buffer and returns the oldest buffer value
* that it replaced
*
* @param newValue
* @return
*/
public float putAndGet(float newValue)
{
float oldestSample = mBuffer[mBufferPointer];
put(newValue);
return oldestSample;
}
/**
* Adds the new value to this buffer.
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the
* newest sample.
* @param index in range of 0 to length-1
* @return value at indexed position
*/
public float get(int index)
{
if(index < mBuffer.length)
{
int pointer = mBufferPointer + index;
if(pointer >= mBuffer.length)
{
pointer -= mBuffer.length;
}
return mBuffer[pointer];
}
else
{
throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" +
mBuffer.length + "]");
}
}
/**
* Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1
* is the newest sample.
*
* @return array of samples
*/
public float[] get()
{
float[] samples = new float[mBuffer.length];
System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer);
if(mBufferPointer != 0)
{
System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer);
}
return samples;
}
}

View File

@@ -0,0 +1,132 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy.hf;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Native buffer implementation for Airspy HF+ & Discovery tuners.
*/
public class AirspyHfNativeBuffer extends AbstractNativeBuffer
{
public static final float SCALE = 1.0f / 32768.0f;
private short[] mInterleavedSamples;
private float mAverageDc;
/**
* Constructs an instance.
* @param timestamp of the first sample of the buffer
* @param interleavedSamples pairs of 16-bit complex samples.
*/
public AirspyHfNativeBuffer(long timestamp, float samplesPerMillisecond, float averageDc, short[] interleavedSamples)
{
super(timestamp, samplesPerMillisecond);
mInterleavedSamples = interleavedSamples;
mAverageDc = averageDc;
}
@Override
public Iterator<ComplexSamples> iterator()
{
return new IteratorComplexSamples();
}
@Override
public Iterator<InterleavedComplexSamples> iteratorInterleaved()
{
return new IteratorInterleaved();
}
@Override
public int sampleCount()
{
return mInterleavedSamples.length / 2;
}
/**
* Scalar implementation of complex samples buffer iterator
*/
public class IteratorComplexSamples implements Iterator<ComplexSamples>
{
private boolean mHasNext = true;
@Override
public boolean hasNext()
{
return mHasNext;
}
@Override
public ComplexSamples next()
{
int length = mInterleavedSamples.length / 2;
float[] i = new float[length];
float[] q = new float[length];
int index = 0;
for(int x = 0; x < mInterleavedSamples.length; x += 2)
{
index = x / 2;
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
i[index] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
q[index] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new ComplexSamples(i, q, getTimestamp());
}
}
/**
* Scalar implementation of interleaved sample buffer iterator.
*/
public class IteratorInterleaved implements Iterator<InterleavedComplexSamples>
{
private boolean mHasNext = true;
@Override
public boolean hasNext()
{
return mHasNext;
}
@Override
public InterleavedComplexSamples next()
{
float[] samples = new float[mInterleavedSamples.length];
for(int x = 0; x < mInterleavedSamples.length; x += 2)
{
//Native ordering is in Q, I, Q, I ... reverse it to I, Q, I, Q
samples[x] = (mInterleavedSamples[x + 1] * SCALE) - mAverageDc;
samples[x + 1] = (mInterleavedSamples[x] * SCALE) - mAverageDc;
}
mHasNext = false;
return new InterleavedComplexSamples(samples, getTimestamp());
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy.hf;
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.DcCorrectionManager;
import io.github.dsheirer.buffer.INativeBuffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
/**
* Airspy HF+ native buffer factory.
*
* Note: the tuner provides 1024 samples in each transfer where samples are 16-bit interleaved complex.
*/
public class AirspyHfNativeBufferFactory extends AbstractNativeBufferFactory
{
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();
/**
* Constructs an instance
*/
public AirspyHfNativeBufferFactory()
{
}
/**
* Converts the samples byte buffer into a native buffer and calculates DC offset
* @param samples byte array copied from native memory
* @param timestamp of the samples
* @return constructed native buffer
*/
@Override
public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
{
ShortBuffer shortBuffer = samples.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short[] converted = new short[shortBuffer.capacity()];
shortBuffer.get(converted);
if(mDcCorrectionManager.shouldCalculateDc())
{
calculateDc(converted);
}
return new AirspyHfNativeBuffer(timestamp, getSamplesPerMillisecond(), mDcCorrectionManager.getAverageDc(), converted);
}
/**
* Calculates the average DC in the sample stream so that it can be subtracted from the samples when the
* native buffer is used.
* @param samples containing DC offset
*/
private void calculateDc(short[] samples)
{
float dcAccumulator = 0;
for(short sample: samples)
{
dcAccumulator += sample;
}
dcAccumulator /= samples.length;
mDcCorrectionManager.adjust(dcAccumulator * AirspyHfNativeBuffer.SCALE);
}
}

View File

@@ -33,52 +33,105 @@ 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 ChannelMetadataPanel mChannelMetadataPanel;
private ChannelDetailPanel mChannelDetailPanel;
private DecodeEventPanel mDecodeEventPanel;
private MessageActivityPanel mMessageActivityPanel;
private ChannelPowerPanel mChannelPowerPanel;
private final ChannelMetadataPanel mChannelMetadataPanel;
private final ChannelDetailPanel mChannelDetailPanel;
private final DecodeEventPanel mDecodeEventPanel;
private final MessageActivityPanel mMessageActivityPanel;
private final ChannelPowerPanel mChannelPowerPanel;
private JideTabbedPane mTabbedPane;
private JideSplitPane mSplitPane;
private boolean mDetailTabsVisible;
/**
* 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)
SettingsManager settingsManager, boolean detailTabsVisible)
{
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);
mDetailTabsVisible = detailTabsVisible;
init();
}
/**
* 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;
}
private void init()
{
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);

View File

@@ -73,7 +73,7 @@ public class DecoderStateEvent
{
StringBuilder sb = new StringBuilder();
sb.append("Decoder State Event - source[").append(mSource.getClass())
sb.append("Decoder State Event - source[").append(mSource != null ? mSource.getClass() : "null")
.append("] event[").append(mEvent)
.append("] state[").append(mState)
.append("] timeslot[").append(mTimeslot)

View File

@@ -59,17 +59,25 @@ public class ControllerPanel extends JPanel
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
TunerManager tunerManager, UserPreferences userPreferences)
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
{
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
playlistManager.getAliasModel());
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager, detailTabsVisible);
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
init();
}
/**
* Now playing panel.
*/
public NowPlayingPanel getNowPlayingPanel()
{
return mNowPlayingPanel;
}
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
@@ -97,8 +105,8 @@ public class ControllerPanel extends JPanel
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"));
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
//Set preferred size to influence the split between these panels

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,22 +14,13 @@
*
* 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;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
@@ -41,12 +31,21 @@ import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
public class ChannelAutoStartFrame extends JFrame
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelAutoStartFrame.class);
private static final int COUNTDOWN_SECONDS = 10;
private Listener<ChannelEvent> mChannelEventListener;
private List<Channel> mChannels;
@@ -55,7 +54,7 @@ public class ChannelAutoStartFrame extends JFrame
private JButton mCancelButton;
private JTable mChannelTable;
private AtomicBoolean mChannelsStarted = new AtomicBoolean();
private int mAutoStartTimeoutSeconds;
private ScheduledFuture<?> mTimerFuture;
/**
@@ -66,10 +65,12 @@ public class ChannelAutoStartFrame extends JFrame
* @param listener to receive channel start/enable request(s)
* @param channels to auto-start
*/
public ChannelAutoStartFrame(Listener<ChannelEvent> listener, List<Channel> channels)
public ChannelAutoStartFrame(Listener<ChannelEvent> listener, List<Channel> channels, UserPreferences userPreferences)
{
mChannelEventListener = listener;
mChannels = channels;
mAutoStartTimeoutSeconds = userPreferences.getApplicationPreference().getChannelAutoStartTimeout();
init();
EventQueue.invokeLater(() -> {
@@ -100,7 +101,7 @@ public class ChannelAutoStartFrame extends JFrame
"[][][grow,fill][]"));
panel.add(new JLabel("The following channels will be automatically"), "span");
mCountdownLabel = new JLabel(getCountdownText(COUNTDOWN_SECONDS));
mCountdownLabel = new JLabel(getCountdownText(mAutoStartTimeoutSeconds));
panel.add(mCountdownLabel, "span");
panel.add(new JScrollPane(getChannelTable()), "span");
@@ -220,7 +221,7 @@ public class ChannelAutoStartFrame extends JFrame
*/
public class CountdownTimer implements Runnable
{
private int mCount = COUNTDOWN_SECONDS;
private int mCount = mAutoStartTimeoutSeconds;
@Override
public void run()

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
@@ -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();
@@ -623,30 +623,27 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
Platform.runLater(() -> channel.setProcessing(false));
}
//Since the processing chain's source will block on stop(), throw it to the thread pool to run
ThreadPool.CACHED.submit(() -> {
try
{
processingChain.stop();
processingChain.removeEventLoggingModules();
processingChain.removeRecordingModules();
try
{
processingChain.stop();
processingChain.removeEventLoggingModules();
processingChain.removeRecordingModules();
//Deregister channel from receive frequency correction events to show in the spectral display (hack!)
processingChain.removeFrequencyChangeListener(channel);
channel.resetFrequencyCorrection();
//Deregister channel from receive frequency correction events to show in the spectral display (hack!)
processingChain.removeFrequencyChangeListener(channel);
channel.resetFrequencyCorrection();
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
mChannelEventBroadcaster.removeListener(processingChain);
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
mChannelEventBroadcaster.removeListener(processingChain);
//Unregister for event bus requests and notifications
processingChain.getEventBus().unregister(ChannelProcessingManager.this);
processingChain.dispose();
}
catch(Exception e)
{
mLog.error("Error during shutdown of processing chain for channel [" + channel.getName() + "}", e);
}
});
//Unregister for event bus requests and notifications
processingChain.getEventBus().unregister(ChannelProcessingManager.this);
processingChain.dispose();
}
catch(Exception e)
{
mLog.error("Error during shutdown of processing chain for channel [" + channel.getName() + "}", e);
}
}
}

View File

@@ -0,0 +1,286 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.calibrate.Calibration;
import io.github.dsheirer.vector.calibrate.CalibrationException;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import org.apache.commons.math3.stat.descriptive.moment.Mean;
/**
* Calibrates AM demodulator options
*/
public class AmDemodulatorCalibration extends Calibration
{
private static final int BUFFER_SIZE = 2048;
private static final int ITERATION_DURATION_MS = 1000;
private static final int WARMUP_ITERATIONS = 5;
private static final int TEST_ITERATIONS = 5;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
private IAmDemodulator mScalarDemodulator = new ScalarAMDemodulator(500.0f);
private IAmDemodulator mVectorDemodulator64 = new VectorAMDemodulator64(500.0f);
private IAmDemodulator mVectorDemodulator128 = new VectorAMDemodulator128(500.0f);
private IAmDemodulator mVectorAMDemodulator256 = new VectorAMDemodulator256(500.0f);
private IAmDemodulator mVectorAMDemodulator512 = new VectorAMDemodulator512(500.0f);
/**
* Constructs an instance
*/
public AmDemodulatorCalibration()
{
super(CalibrationType.AM_DEMODULATOR);
}
@Override public void calibrate() throws CalibrationException
{
float[] magnitude = getFloatSamples(BUFFER_SIZE);
Mean scalarMean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testScalar(magnitude);
scalarMean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult()));
Mean vector64Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector64(magnitude);
vector64Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 64: " + DECIMAL_FORMAT.format(vector64Mean.getResult()));
Mean vector128Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector128(magnitude);
vector128Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 128: " + DECIMAL_FORMAT.format(vector128Mean.getResult()));
Mean vector256Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector256(magnitude);
vector256Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 256: " + DECIMAL_FORMAT.format(vector256Mean.getResult()));
Mean vector512Mean = new Mean();
for(int x = 0; x < WARMUP_ITERATIONS; x++)
{
long score = testVector512(magnitude);
vector512Mean.increment(score);
}
mLog.info("AM DEMODULATOR WARMUP - VECTOR 512: " + DECIMAL_FORMAT.format(vector512Mean.getResult()));
//Start tests
scalarMean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testScalar(magnitude);
scalarMean.increment(score);
}
mLog.info("AM DEMODULATOR - SCALAR: " + DECIMAL_FORMAT.format(scalarMean.getResult()));
vector64Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector64(magnitude);
vector64Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 64: " + DECIMAL_FORMAT.format(vector64Mean.getResult()));
vector128Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector128(magnitude);
vector128Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 128: " + DECIMAL_FORMAT.format(vector128Mean.getResult()));
vector256Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector256(magnitude);
vector256Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 256: " + DECIMAL_FORMAT.format(vector256Mean.getResult()));
vector512Mean.clear();
for(int x = 0; x < TEST_ITERATIONS; x++)
{
long score = testVector512(magnitude);
vector512Mean.increment(score);
}
mLog.info("AM DEMODULATOR - VECTOR 512: " + DECIMAL_FORMAT.format(vector512Mean.getResult()));
Implementation optimal = Implementation.SCALAR;
double optimalResult = scalarMean.getResult();
if(vector64Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_64;
optimalResult = vector64Mean.getResult();
}
if(vector128Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_128;
optimalResult = vector128Mean.getResult();
}
if(vector256Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_256;
optimalResult = vector256Mean.getResult();
}
if(vector512Mean.getResult() > optimalResult)
{
optimal = Implementation.VECTOR_SIMD_512;
}
setImplementation(optimal);
mLog.info("AM DEMODULATOR - SET OPTIMAL IMPLEMENTATION TO:" + getImplementation());
}
private long testScalar(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mScalarDemodulator.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector64(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorDemodulator64.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector128(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorDemodulator128.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector256(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorAMDemodulator256.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
private long testVector512(float[] magnitude)
{
double accumulator = 0.0;
long count = 0;
long start = System.currentTimeMillis();
while((System.currentTimeMillis() - start) < ITERATION_DURATION_MS)
{
float[] demodulated = mVectorAMDemodulator512.demodulateMagnitude(magnitude);
accumulator += demodulated[1];
count++;
}
return count + (long)(accumulator * 0);
}
public static void main(String[] args)
{
AmDemodulatorCalibration calibration = new AmDemodulatorCalibration();
try
{
calibration.calibrate();
}
catch(Exception e)
{
mLog.error("Error during calibration", e);
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Factory for creating AM demodulators
*/
public class AmDemodulatorFactory
{
private static final Logger mLog = LoggerFactory.getLogger(AmDemodulatorFactory.class);
/**
* Creates the optimal AM demodulator using calibration data to select the optimal
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static IAmDemodulator getAmDemodulator(float gain)
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.AM_DEMODULATOR);
switch(implementation)
{
case VECTOR_SIMD_64:
return new VectorAMDemodulator64(gain);
case VECTOR_SIMD_128:
return new VectorAMDemodulator128(gain);
case VECTOR_SIMD_256:
return new VectorAMDemodulator256(gain);
case VECTOR_SIMD_512:
return new VectorAMDemodulator512(gain);
case VECTOR_SIMD_PREFERRED:
return new VectorAMDemodulator64(gain);
case SCALAR:
case UNCALIBRATED:
return new ScalarAMDemodulator(gain);
default:
mLog.warn("Unrecognized optimal operation for AM demodulator: " + implementation.name());
return new ScalarAMDemodulator(gain);
}
}
}

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* 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.dsp.am;
/**
@@ -6,10 +25,9 @@ package io.github.dsheirer.dsp.am;
public interface IAmDemodulator
{
/**
* Demodulate the complex I & Q sample arrays
* @param i samples
* @param q samples
* @return demodulated samples.
* Demodulates an array of vector squared magnitudes.
* @param magnitude values that are squared.
* @return demodulated samples
*/
float[] demodulate(float[] i, float[] q);
float[] demodulateMagnitude(float[] magnitude);
}

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
@@ -23,7 +23,7 @@ import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class AMDemodulator implements IAmDemodulator
public class ScalarAMDemodulator implements IAmDemodulator
{
private float mGain;
@@ -32,7 +32,7 @@ public class AMDemodulator implements IAmDemodulator
*
* @param gain to apply to demodulated output samples.
*/
public AMDemodulator(float gain)
public ScalarAMDemodulator(float gain)
{
mGain = gain;
}
@@ -57,13 +57,13 @@ public class AMDemodulator implements IAmDemodulator
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override public float[] demodulate(float[] i, float[] q)
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
float[] demodulated = new float[i.length];
for(int x = 0; x < i.length; x++)
float[] demodulated = new float[magnitude.length];
for(int x = 0; x < magnitude.length; x++)
{
demodulated[x] = (float)FastMath.sqrt((i[x] * i[x]) + (q[x] * q[x])) * mGain;
demodulated[x] = (float)FastMath.sqrt(magnitude[x]) * mGain;
}
return demodulated;

View File

@@ -0,0 +1,161 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.fm.ISquelchingDemodulator;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.dsp.squelch.AdaptiveSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* AM demodulator with integrated squelch control.
*/
public class SquelchingAMDemodulator implements ISquelchingDemodulator, Listener<SourceEvent>
{
private static final float ZERO = 0.0f;
private final IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
private final IAmDemodulator mAmDemodulator;
private final AdaptiveSquelch mAdaptiveSquelch;
private boolean mSquelchChanged = false;
/**
* Constructs an instance
* @param gain to apply to the demodulated AM samples (e.g. 500.0f)
* @param alpha decay value of the single pole IIR filter in range: 0.0 - 1.0. The smaller the alpha value,
* the slower the squelch response.
* @param squelchThreshold in decibels. Signal power must exceed this threshold value for unsquelch.
* @param squelchAutoTrack to enable the squelch noise floor auto tracking feature.
*/
public SquelchingAMDemodulator(float gain, float alpha, float squelchThreshold, boolean squelchAutoTrack)
{
mAmDemodulator = AmDemodulatorFactory.getAmDemodulator(gain);
mAdaptiveSquelch = new AdaptiveSquelch(alpha, squelchThreshold, squelchAutoTrack);
}
/**
* Set or update the sample rate for the squelch to adjust the power level notification rate.
* @param sampleRate in hertz
*/
public void setSampleRate(int sampleRate)
{
mAdaptiveSquelch.setSampleRate(sampleRate);
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mAdaptiveSquelch.setSourceEventListener(listener);
}
/**
* Demodulates the complex sample arrays.
* @param i inphase sample array
* @param q quadrature sample array
* @return demodulated AM samples with gain applied.
*/
public float[] demodulate(float[] i, float[] q)
{
mAdaptiveSquelch.setSquelchChanged(false);
setSquelchChanged(false);
float[] magnitude = mMagnitudeCalculator.calculate(i, q);
float[] demodulated = mAmDemodulator.demodulateMagnitude(magnitude);
for(int x = 0; x < i.length; x++)
{
mAdaptiveSquelch.process(magnitude[x]);
if(!(mAdaptiveSquelch.isUnmuted()))
{
demodulated[x] = ZERO;
}
if(mAdaptiveSquelch.isSquelchChanged())
{
setSquelchChanged(true);
}
}
return demodulated;
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(float threshold)
{
mAdaptiveSquelch.setSquelchThreshold(threshold);
}
@Override
public void setSquelchAutoTrack(boolean autoTrack)
{
mAdaptiveSquelch.setSquelchAutoTrack(autoTrack);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mAdaptiveSquelch.isMuted();
}
/**
* Process source events initiated by the timer and end-user.
* @param sourceEvent to process.
*/
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
//Only forward squelch threshold & auto-track request events
case REQUEST_CURRENT_SQUELCH_THRESHOLD:
case REQUEST_CHANGE_SQUELCH_THRESHOLD:
case REQUEST_CURRENT_SQUELCH_AUTO_TRACK:
case REQUEST_CHANGE_SQUELCH_AUTO_TRACK:
{
mAdaptiveSquelch.receive(sourceEvent);
}
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator128 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator128(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator256 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator256(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator512 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator512(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

View File

@@ -0,0 +1,83 @@
/*
* *****************************************************************************
* 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.dsp.am;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
import org.apache.commons.math3.util.FastMath;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class VectorAMDemodulator64 implements IAmDemodulator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
private float mGain;
private IMagnitudeCalculator mMagnitudeCalculator = MagnitudeFactory.getMagnitudeCalculator();
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public VectorAMDemodulator64(float gain)
{
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float) FastMath.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
@Override
public float[] demodulateMagnitude(float[] magnitude)
{
VectorUtilities.checkArrayLength(magnitude, VECTOR_SPECIES);
FloatVector gain = FloatVector.broadcast(VECTOR_SPECIES, mGain);
float[] demodulated = new float[magnitude.length];
FloatVector vector;
for(int x = 0; x < magnitude.length; x += VECTOR_SPECIES.length())
{
vector = FloatVector.fromArray(VECTOR_SPECIES, magnitude, x);
vector.sqrt().mul(gain).intoArray(demodulated, x);
}
return demodulated;
}
}

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
@@ -122,6 +122,11 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
mChannels.add(polyphaseChannelSource);
mSourceChangeBroadcaster.addListener(polyphaseChannelSource.getSourceEventListener());
}
else
{
mLog.error("Error adding polyphase channel source - " + (polyphaseChannelSource == null ? "source is null" :
"channel source is already added to this channelizer"));
}
}
/**
@@ -136,6 +141,11 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
mChannels.remove(polyphaseChannelSource);
mSourceChangeBroadcaster.removeListener(polyphaseChannelSource.getSourceEventListener());
}
else
{
mLog.error("Error removing polyphase channel source - " + (polyphaseChannelSource == null ? "source is null" :
"channel source was not previously added to this channelizer"));
}
}
/**

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
@@ -24,7 +24,6 @@ import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.util.Dispatcher;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
@@ -69,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
//Sized at 152 buffers a second where max = 5 seconds
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(5 * 152);
//Sized to process 40 times per second
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(25);
private FloatFFT_1D mFFT;
private float[] mInlineSamples;
private float[] mInlineFilter;
@@ -407,25 +406,35 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
*/
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
{
public IFFTProcessorDispatcher(int maximumSize)
public IFFTProcessorDispatcher(long interval)
{
super(maximumSize, "sdrtrunk polyphase ifft processor", Collections.emptyList());
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
//channel results array contained in each results buffer and then dispatch the buffer
//so that it can be distributed to each channel listener.
setListener(list -> {
List<float[]> processedChannelResults = new ArrayList<>();
for(float[] channelResults: list)
try
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
processedChannelResults.add(channelResults);
}
List<float[]> processedChannelResults = new ArrayList<>();
dispatch(processedChannelResults);
for(float[] channelResults: list)
{
if(channelResults != null)
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
processedChannelResults.add(channelResults);
}
}
dispatch(processedChannelResults);
}
catch(Throwable t)
{
mLog.error("Error during IFFT and dispatch of processed channel results", t);
}
});
}
}

View File

@@ -20,15 +20,14 @@ package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.INativeBufferProvider;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.source.ISourceEventProcessor;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerController;
@@ -44,7 +43,6 @@ import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -68,6 +66,7 @@ import org.slf4j.LoggerFactory;
public class PolyphaseChannelManager implements ISourceEventProcessor
{
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LoggerFactory.getLogger(PolyphaseChannelManager.class));
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
private static final double CHANNEL_OVERSAMPLING = 2.0;
@@ -111,8 +110,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
mBufferDispatcher = new Dispatcher(500, "sdrtrunk polyphase buffer processor",
new NativeBufferPoisonPill());
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
mBufferDispatcher.setListener(mNativeBufferReceiver);
}
@@ -178,14 +176,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
}
/**
* Current channel sample rate which is (2 * channel bandwidth).
*/
public double getChannelSampleRate()
{
return mChannelCalculator.getChannelSampleRate();
}
/**
* Current channel bandwidth/spacing.
*/
@@ -215,7 +205,8 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
catch(IllegalArgumentException iae)
{
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
LOGGING_SUPPRESSOR.error(iae.getMessage(), 3, "Couldn't allocate channel. " + iae.getMessage());
channelSource = null;
}
}
@@ -440,9 +431,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
}
break;
case REQUEST_STOP_SAMPLE_STREAM:
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource channelSource)
{
stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
stopChannelSource(channelSource);
channelSource.dispose();
}
else
{
@@ -450,14 +442,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
}
break;
case REQUEST_SOURCE_DISPOSE:
Source source = sourceEvent.getSource();
if(source instanceof PolyphaseChannelSource)
{
source.dispose();
}
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast so that the tuner source can process this event
mSourceEventBroadcaster.broadcast(sourceEvent);
@@ -481,7 +465,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
public class NativeBufferReceiver implements Listener<INativeBuffer>
{
private boolean mOutputProcessorUpdateRequired = false;
private ReentrantLock mUpdateFlagLock = new ReentrantLock();
/**
* Processes tuner center frequency change source events to flag when output processors need updating.
@@ -493,18 +476,9 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
if(mChannelCalculator.getCenterFrequency() != frequency)
{
mUpdateFlagLock.lock();
try
{
//Update the channel calculator frequency so that it's ready when the output processor update occurs
mChannelCalculator.setCenterFrequency(frequency);
mOutputProcessorUpdateRequired = true;
}
finally
{
mUpdateFlagLock.unlock();
}
//Update the channel calculator frequency so that it's ready when the output processor update occurs
mChannelCalculator.setCenterFrequency(frequency);
mOutputProcessorUpdateRequired = true;
}
}
@@ -516,19 +490,17 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
@Override
public void receive(INativeBuffer nativeBuffer)
{
mUpdateFlagLock.lock();
try
if(mOutputProcessorUpdateRequired)
{
if(mOutputProcessorUpdateRequired)
try
{
updateOutputProcessors();
mOutputProcessorUpdateRequired = false;
}
}
finally
{
mUpdateFlagLock.unlock();
catch(Exception e)
{
mLog.error("Error updating polyphase channel output processors");
}
mOutputProcessorUpdateRequired = false;
}
if(mPolyphaseChannelizer != null)

View File

@@ -25,12 +25,10 @@ import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.StreamProcessorWithHeartbeat;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,15 +41,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
private Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
private Listener<ComplexSamples> mSamplesListener;
private double mChannelSampleRate;
private long mIndexCenterFrequency;
private ReentrantLock mOutputProcessorLock = new ReentrantLock();
private List<Integer> mOutputProcessorIndexes = new ArrayList<>();
private double mTunerSampleRate;
private double mTunerCenterFrequency;
private PendingOutputProcessorUpdate mPendingOutputProcessorUpdate;
/**
* Constructs an instance
@@ -67,8 +63,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
super(producerSourceEventListener, tunerChannel);
mChannelSampleRate = channelCalculator.getChannelSampleRate();
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
updateOutputProcessor(channelCalculator, filterManager);
doUpdateOutputProcessor(channelCalculator, filterManager);
}
/**
@@ -111,8 +106,6 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
super.start();
mStreamHeartbeatProcessor.start();
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.start();
@@ -128,8 +121,6 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
{
mPolyphaseChannelOutputProcessor.stop();
}
mStreamHeartbeatProcessor.stop();
}
/**
@@ -138,112 +129,129 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
@Override
public void setListener(final Listener<ComplexSamples> listener)
{
mStreamHeartbeatProcessor.setListener(listener);
mSamplesListener = listener;
mPolyphaseChannelOutputProcessor.setListener(this);
}
@Override
public void receive(ComplexSamples complexSamples)
{
mStreamHeartbeatProcessor.receive(complexSamples);
if(mSamplesListener != null)
{
try
{
mSamplesListener.receive(complexSamples);
}
catch(Throwable t)
{
mLog.error("Error dispatching complex samples to listener [" + mSamplesListener.getClass() + "]", t);
}
}
}
/**
* Updates the output processor whenever the source tuner's center frequency changes.
* Queues a request to update the output processor whenever the source tuner's center frequency changes.
* @param channelCalculator providing access to updated tuner center frequency, sample rate, etc.
* @param filterManager for designing and caching synthesis filters
*/
public void updateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager) throws IllegalArgumentException
public void updateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager)
throws IllegalArgumentException
{
mPendingOutputProcessorUpdate = new PendingOutputProcessorUpdate(channelCalculator, filterManager);
}
/**
* Performs the output processor update. This method must be invoked on the channel results calling thread to
* ensure we don't have thread contention on the output processor.
* @param channelCalculator for updates
* @param filterManager for updates
* @throws IllegalArgumentException if there's an issue.
*/
public void doUpdateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager)
throws IllegalArgumentException
{
String errorMessage = null;
mOutputProcessorLock.lock();
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = channelCalculator.getChannelIndexes(getTunerChannel());
try
mOutputProcessorIndexes.clear();
mOutputProcessorIndexes.addAll(indexes);
mTunerCenterFrequency = channelCalculator.getCenterFrequency();
mTunerSampleRate = channelCalculator.getSampleRate();
//The provided channels are necessarily aligned to the center frequency that this source is providing and an
//oscillator will mix the provided channels to bring the desired center frequency to baseband.
setFrequency(channelCalculator.getCenterFrequencyForIndexes(indexes));
if(mPolyphaseChannelOutputProcessor != null && mPolyphaseChannelOutputProcessor.getInputChannelCount() == indexes.size())
{
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = channelCalculator.getChannelIndexes(getTunerChannel());
mPolyphaseChannelOutputProcessor.setPolyphaseChannelIndices(indexes);
mOutputProcessorIndexes.clear();
mOutputProcessorIndexes.addAll(indexes);
mTunerCenterFrequency = channelCalculator.getCenterFrequency();
mTunerSampleRate = channelCalculator.getSampleRate();
//The provided channels are necessarily aligned to the center frequency that this source is providing and an
//oscillator will mix the provided channels to bring the desired center frequency to baseband.
setFrequency(channelCalculator.getCenterFrequencyForIndexes(indexes));
if(mPolyphaseChannelOutputProcessor != null && mPolyphaseChannelOutputProcessor.getInputChannelCount() == indexes.size())
if(indexes.size() > 1)
{
mPolyphaseChannelOutputProcessor.setPolyphaseChannelIndices(indexes);
if(indexes.size() > 1)
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), indexes.size());
mPolyphaseChannelOutputProcessor.setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
errorMessage ="Cannot update output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices - channel sample rate [" + channelCalculator.getChannelSampleRate() +
"] channel bandwidth [" + channelCalculator.getChannelBandwidth() + "]";
}
}
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
else //Create a new output processor.
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.setListener(null);
mPolyphaseChannelOutputProcessor.stop();
}
mPolyphaseChannelOutputProcessor = null;
switch(indexes.size())
{
case 1:
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, channelCalculator.getChannelCount(), getHeartbeatManager());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
break;
case 2:
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), indexes.size());
mPolyphaseChannelOutputProcessor.setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
errorMessage ="Cannot update output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
}
}
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
else //Create a new output processor.
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.setListener(null);
}
mPolyphaseChannelOutputProcessor = null;
switch(indexes.size())
{
case 1:
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, channelCalculator.getChannelCount());
channelCalculator.getChannelBandwidth(), 2);
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, filter, channelCalculator.getChannelCount(), getHeartbeatManager());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
break;
case 2:
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), 2);
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, filter, channelCalculator.getChannelCount());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
}
catch(FilterDesignException fde)
{
errorMessage = "Cannot create new output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
mLog.error("Error creating a synthesis filter for a new channel output processor");
}
break;
default:
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(channelCalculator.toString());
errorMessage = "Unable to create new channel output processor - unexpected channel index size: " +
indexes.size();
}
}
catch(FilterDesignException fde)
{
errorMessage = "Cannot create new output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
mLog.error("Error creating a synthesis filter for a new channel output processor");
}
break;
default:
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(channelCalculator.toString());
errorMessage = "Unable to create new channel output processor - unexpected channel index size: " +
indexes.size();
}
}
finally
{
mOutputProcessorLock.unlock();
}
//Unlikely, but if we had an error designing a synthesis filter, throw an exception
if(errorMessage != null)
@@ -262,7 +270,13 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
*/
public void receiveChannelResults(List<float[]> channelResultsList, long currentSamplesTimestamp)
{
mOutputProcessorLock.lock();
if(mPendingOutputProcessorUpdate != null)
{
ChannelCalculator channelCalculator = mPendingOutputProcessorUpdate.getChannelCalculator();
SynthesisFilterManager filterManager = mPendingOutputProcessorUpdate.getSynthesisFilterManager();
mPendingOutputProcessorUpdate = null;
doUpdateOutputProcessor(channelCalculator, filterManager);
}
try
{
@@ -271,9 +285,9 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList, currentSamplesTimestamp);
}
}
finally
catch(Exception e)
{
mOutputProcessorLock.unlock();
mLog.error("Error processing channel results", e);
}
}
@@ -342,4 +356,40 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
getTunerChannel().getFrequency();
}
/**
* Support classes needed to update the output processor.
*/
public class PendingOutputProcessorUpdate
{
private final ChannelCalculator mChannelCalculator;
private final SynthesisFilterManager mSynthesisFilterManager;
/**
* Constructs an instance
* @param channelCalculator for support
* @param synthesisFilterManager for support
*/
public PendingOutputProcessorUpdate(ChannelCalculator channelCalculator, SynthesisFilterManager synthesisFilterManager)
{
mChannelCalculator = channelCalculator;
mSynthesisFilterManager = synthesisFilterManager;
}
/**
* Channel calculator
*/
public ChannelCalculator getChannelCalculator()
{
return mChannelCalculator;
}
/**
* Channel calculator
*/
public SynthesisFilterManager getSynthesisFilterManager()
{
return mSynthesisFilterManager;
}
}
}

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,8 +20,8 @@ package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import io.github.dsheirer.util.Dispatcher;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -31,6 +31,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class);
private Dispatcher<List<float[]>> mChannelResultsDispatcher;
private HeartbeatManager mHeartbeatManager;
protected Listener<ComplexSamples> mComplexSamplesListener;
private int mInputChannelCount;
private long mCurrentSampleTimestamp = System.currentTimeMillis();
@@ -42,12 +43,24 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
* @param inputChannelCount is the number of input channels for this output processor
* @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
* channel sample rate for frequency translation/correction.
* @param heartbeatManager to receive pings on the dispatcher thread
*/
public ChannelOutputProcessor(int inputChannelCount, double sampleRate)
public ChannelOutputProcessor(int inputChannelCount, double sampleRate, HeartbeatManager heartbeatManager)
{
mInputChannelCount = inputChannelCount;
mChannelResultsDispatcher = new Dispatcher<>((int)sampleRate, "sdrtrunk polyphase channel", Collections.emptyList());
mChannelResultsDispatcher.setListener(floats -> process(floats));
//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",50, mHeartbeatManager);
mChannelResultsDispatcher.setListener(floats -> {
try
{
process(floats);
}
catch(Throwable t)
{
mLog.error("Error processing channel results", t);
}
});
}
/**

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,10 +37,12 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
* @param sampleRate of the output sample stream.
* @param channelIndexes containing a single channel index.
* @param gain value to apply. This is typically the same as the channelizer's channel count.
* @param heartbeatManager to receive heartbeats on the dispatch thread
*/
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float gain)
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float gain,
HeartbeatManager heartbeatManager)
{
super(1, sampleRate);
super(1, sampleRate, heartbeatManager);
setPolyphaseChannelIndices(channelIndexes);
mMixerAssembler = new OneChannelMixerAssembler(gain);
mMixerAssembler.getMixer().setSampleRate(sampleRate);

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.source.heartbeat.HeartbeatManager;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,12 +38,14 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
* @param sampleRate of the output sample stream.
* @param channelIndexes containing two channel indices.
* @param gain to apply to output. Typically this is equal to the channelizer's channel count.
* @param heartbeatManager to be pinged on the dispatcher thread
*/
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter, float gain)
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter, float gain,
HeartbeatManager heartbeatManager)
{
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
//after synthesizing both input channels
super(2, sampleRate);
super(2, sampleRate, heartbeatManager);
setPolyphaseChannelIndices(channelIndexes);
mMixerAssembler = new TwoChannelMixerAssembler(gain);
mMixerAssembler.getMixer().setSampleRate(sampleRate);

View File

@@ -1,3 +1,22 @@
/*
* *****************************************************************************
* 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.dsp.filter.iir;
/**
@@ -40,7 +59,7 @@ public class SinglePoleIirFilter
* @param sample to process/filter
* @return filtered output
*/
public double filter(float sample)
public float filter(float sample)
{
mOutput = (mOutput * mOneMinusAlpha) + (mAlpha * sample);
return mOutput;
@@ -49,7 +68,7 @@ public class SinglePoleIirFilter
/**
* Current output value
*/
public double getValue()
public float getValue()
{
return mOutput;
}

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
@@ -37,40 +37,23 @@ public class FmDemodulatorFactory
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static IFmDemodulator getFmDemodulator()
public static IDemodulator getFmDemodulator()
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.FM_DEMODULATOR);
switch(implementation)
{
case VECTOR_SIMD_PREFERRED:
return new VectorFMDemodulator();
case VECTOR_SIMD_64:
return new VectorFMDemodulator64();
case VECTOR_SIMD_128:
return new VectorFMDemodulator128();
case VECTOR_SIMD_256:
return new VectorFMDemodulator256();
case VECTOR_SIMD_512:
return new VectorFMDemodulator512();
case SCALAR:
return new ScalarFMDemodulator();
default:
mLog.warn("Unrecognized optimal operation for FM demodulator: " + implementation.name());
return new ScalarFMDemodulator();
}
}
/**
* Creates the optimal Squelching FM demodulator using calibration data to select the optimal
* implementation from scalar and vector options.
* @return demodulator instance
*/
public static ISquelchingFmDemodulator getSquelchingFmDemodulator(float alpha, float threshold, int ramp)
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.FM_DEMODULATOR);
switch(implementation)
{
case VECTOR_SIMD_PREFERRED:
return new VectorSquelchingFMDemodulator(alpha, threshold, ramp);
case SCALAR:
return new ScalarSquelchingFMDemodulator(alpha, threshold, ramp);
default:
mLog.warn("Unrecognized optimal operation for Squelching FM demodulator: " + implementation.name());
return new ScalarSquelchingFMDemodulator(alpha, threshold, ramp);
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* *****************************************************************************
* 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.dsp.fm;
/**
* Interface for demodulator of complex sample arrays.
*/
public interface IDemodulator
{
/**
* Demodulate the complex sample array and return an array of real audio samples.
* @param i samples to demodulate
* @param q samples to demodulate
* @return demodulated samples.
*/
float[] demodulate(float[] i, float[] q);
}

View File

@@ -1,14 +0,0 @@
package io.github.dsheirer.dsp.fm;
/**
* Interface for FM demodulator
*/
public interface IFmDemodulator
{
/**
* Demodulate the complex sample array and return an array of real audio samples.
* @param samples to demodulate
* @return demodulated samples.
*/
float[] demodulate(float[] i, float[] q);
}

View File

@@ -0,0 +1,69 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* Interface for FM demodulator that provides squelch control.
*/
public interface ISquelchingDemodulator extends IDemodulator
{
/**
* Sets the sample rate so that the squelch can adjust the channel power level broadcast notification rate
* @param sampleRate in hertz
*/
void setSampleRate(int sampleRate);
/**
* Indicates if the squelch state has changed.
*/
boolean isSquelchChanged();
/**
* Indicates if the squelch state is currently muted
*/
boolean isMuted();
/**
* Sets the threshold to use for power squelch
* @param threshold in decibels
*/
void setSquelchThreshold(float threshold);
/**
* Enables or disables the squelch auto-tracking feature where the squelch threshold is auto-adjusted based on the
* observed channel power when the signal is not present (ie squelch state).
* @param autoTrack true to enable
*/
void setSquelchAutoTrack(boolean autoTrack);
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
void setSourceEventListener(Listener<SourceEvent> listener);
/**
* Receives a source event
*/
void receive(SourceEvent sourceEvent);
}

View File

@@ -1,42 +0,0 @@
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* Interface for FM demodulator that provides squelch control.
*/
public interface ISquelchingFmDemodulator extends IFmDemodulator
{
/**
* Indicates if the squelch state has changed.
*/
boolean isSquelchChanged();
/**
* Indicates if the squelch state is currently muted
*/
boolean isMuted();
/**
* Sets the threshold to use for power squelch
* @param threshold in decibels
*/
void setSquelchThreshold(double threshold);
/**
* Resets the demodulator
*/
void reset();
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
void setSourceEventListener(Listener<SourceEvent> listener);
/**
* Receives a source event
*/
void receive(SourceEvent sourceEvent);
}

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,16 +18,15 @@
*/
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*/
public class ScalarFMDemodulator implements IFmDemodulator
public class ScalarFMDemodulator implements IDemodulator
{
private float mPreviousI = 0.0f;
private float mPreviousQ = 0.0f;
protected float mPreviousI = 0.0f;
protected float mPreviousQ = 0.0f;
protected float mGain;
/**
@@ -47,50 +46,6 @@ public class ScalarFMDemodulator implements IFmDemodulator
mGain = gain;
}
/**
* Demodulates the I/Q sample via complex sample multiplication by the complex conjugates of the most recently
* demodulated complex sample (ie previous sample). Each new sample overwrites the previously stored sample to
* allow future invocations of the method to simply use a new sample value.
*
* @param currentI of the sample
* @param currentQ of the sample
* @return demodulated sample
*/
public float demodulate(float currentI, float currentQ)
{
/**
* Multiply the current sample against the complex conjugate of the
* previous sample to derive the phase delta between the two samples
*
* Negating the previous sample quadrature produces the conjugate
*/
double inphase = (currentI * mPreviousI) - (currentQ * -mPreviousQ);
double quadrature = (currentQ * mPreviousI) + (currentI * -mPreviousQ);
double angle = 0.0f;
//Check for divide by zero
if(inphase != 0)
{
/**
* Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal
*/
double denominator = 1.0d / inphase;
angle = FastMath.atan((double)quadrature * denominator);
}
/**
* Store the current sample to use during the next iteration
*/
mPreviousI = currentI;
mPreviousQ = currentQ;
return (float)(angle * mGain);
}
public float[] demodulate(float[] i, float[] q)
{
float[] demodulated = new float[i.length];
@@ -135,35 +90,6 @@ public class ScalarFMDemodulator implements IFmDemodulator
return demodulated;
}
/**
* Demodulates the complex samples and returns the demodulated value.
* @param previous
* @param current
* @return
*/
public static double demodulate(Complex previous, Complex current)
{
double inphase = (current.inphase() * previous.inphase()) - (current.quadrature() * -previous.quadrature());
double quadrature = (current.quadrature() * previous.inphase()) + (current.inphase() * -previous.quadrature());
double angle = 0.0f;
//Check for divide by zero
if(inphase != 0)
{
/**
* Use the arc-tangent of quadrature divided by inphase to
* get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal
*/
double denominator = 1.0d / inphase;
angle = FastMath.atan((double)quadrature * denominator);
}
return angle;
}
public void dispose()
{
//no-op

View File

@@ -1,98 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.dsp.squelch.PowerSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*
* Implements listener of source events to process runtime squelch threshold change request events
* which are forwarded to the power squelch control.
*/
public class ScalarSquelchingFMDemodulator extends ScalarFMDemodulator implements ISquelchingFmDemodulator, Listener<SourceEvent>
{
private PowerSquelch mPowerSquelch;
private boolean mSquelchChanged = false;
/**
* Creates an FM demodulator instance with a default gain of 1.0.
*/
public ScalarSquelchingFMDemodulator(float alpha, float threshold, int ramp)
{
mPowerSquelch = new PowerSquelch(alpha, threshold, ramp);
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mPowerSquelch.setSourceEventListener(listener);
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(double threshold)
{
mPowerSquelch.setSquelchThreshold(threshold);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mPowerSquelch.isMuted();
}
@Override
public void receive(SourceEvent sourceEvent)
{
//Only forward squelch threshold change request events
if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
}
}

View File

@@ -0,0 +1,161 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.dsp.magnitude.IMagnitudeCalculator;
import io.github.dsheirer.dsp.magnitude.MagnitudeFactory;
import io.github.dsheirer.dsp.squelch.AdaptiveSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*
* Implements listener of source events to process runtime squelch threshold change request events
* which are forwarded to the power squelch control.
*/
public class SquelchingFMDemodulator implements ISquelchingDemodulator, Listener<SourceEvent>
{
private static final Logger mLog = LoggerFactory.getLogger(SquelchingFMDemodulator.class);
private static final float ZERO = 0.0f;
private final AdaptiveSquelch mAdaptiveSquelch;
private final IMagnitudeCalculator mMagnitude = MagnitudeFactory.getMagnitudeCalculator();
private final IDemodulator mFmDemodulator = FmDemodulatorFactory.getFmDemodulator();
private boolean mSquelchChanged = false;
/**
* Creates an FM demodulator instance with a default gain of 1.0.
*
* @param alpha decay value of the single pole IIR filter in range: 0.0 - 1.0. The smaller the alpha value,
* the slower the squelch response.
* @param squelchThreshold in decibels. Signal power must exceed this threshold value for unsquelch.
* causes immediate mute and unmute. Set to higher count to prevent mute/unmute flapping.
* @param squelchAutoTrack to enable the squelch noise floor auto-tracking feature.
*/
public SquelchingFMDemodulator(float alpha, float squelchThreshold, boolean squelchAutoTrack)
{
mAdaptiveSquelch = new AdaptiveSquelch(alpha, squelchThreshold, squelchAutoTrack);
}
/**
* Set or update the sample rate for the squelch to adjust the power level notification rate.
* @param sampleRate in hertz
*/
public void setSampleRate(int sampleRate)
{
mAdaptiveSquelch.setSampleRate(sampleRate);
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mAdaptiveSquelch.setSourceEventListener(listener);
}
/**
* Demodulates the complex (I/Q) sample arrays
* @param i inphase samples
* @param q quadrature samples
* @return demodulated real samples
*/
@Override
public float[] demodulate(float[] i, float[] q)
{
setSquelchChanged(false);
float[] demodulated = mFmDemodulator.demodulate(i, q);
float[] magnitude = mMagnitude.calculate(i, q);
for(int x = 0; x < magnitude.length; x++)
{
mAdaptiveSquelch.process(magnitude[x]);
if(!(mAdaptiveSquelch.isUnmuted()))
{
demodulated[x] = ZERO;
}
if(mAdaptiveSquelch.isSquelchChanged())
{
setSquelchChanged(true);
}
}
return demodulated;
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(float threshold)
{
mAdaptiveSquelch.setSquelchThreshold(threshold);
}
@Override
public void setSquelchAutoTrack(boolean autoTrack)
{
mAdaptiveSquelch.setSquelchAutoTrack(autoTrack);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mAdaptiveSquelch.isMuted();
}
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
//Only forward squelch threshold and auto-track current & change requests
case REQUEST_CURRENT_SQUELCH_THRESHOLD:
case REQUEST_CHANGE_SQUELCH_THRESHOLD:
case REQUEST_CURRENT_SQUELCH_AUTO_TRACK:
case REQUEST_CHANGE_SQUELCH_AUTO_TRACK:
mAdaptiveSquelch.receive(sourceEvent);
break;
}
}
}

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,17 +26,12 @@ import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Random;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator implements IFmDemodulator
public abstract class VectorFMDemodulator implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator.class);
protected static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_PREFERRED;
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
@@ -48,9 +43,16 @@ public class VectorFMDemodulator implements IFmDemodulator
{
}
/**
* Vector species as specified by the implementing sub-class.
*/
protected abstract VectorSpecies<Float> getVectorSpecies();
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
// VectorSpecies<Float> species= getVectorSpecies();
VectorUtilities.checkComplexArrayLength(i, q, getVectorSpecies());
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
@@ -70,12 +72,12 @@ public class VectorFMDemodulator implements IFmDemodulator
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += getVectorSpecies().length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
previousI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(getVectorSpecies(), mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(getVectorSpecies(), mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
@@ -89,53 +91,4 @@ public class VectorFMDemodulator implements IFmDemodulator
return demodulated;
}
public static void main(String[] args)
{
int iterations = 3_000_000;
int sampleSize = 2048;
Random random = new Random();
float[] iSamples = new float[sampleSize];
float[] qSamples = new float[sampleSize];
for(int x = 0; x < iSamples.length; x++)
{
iSamples[x] = random.nextFloat() * 2.0f - 1.0f;
qSamples[x] = random.nextFloat() * 2.0f - 1.0f;
}
boolean validation = false;
ScalarFMDemodulator legacy = new ScalarFMDemodulator();
VectorFMDemodulator vector = new VectorFMDemodulator();
if(validation)
{
float[] legacySamples = legacy.demodulate(iSamples, qSamples);
float[] vectorSamples = vector.demodulate(iSamples, qSamples);
System.out.println("LEGACY:" + Arrays.toString(legacySamples));
System.out.println("VECTOR:" + Arrays.toString(vectorSamples));
}
else
{
System.out.println("Test Starting ...");
long start = System.currentTimeMillis();
double accumulator = 0.0;
for(int i = 0; i < iterations; i++)
{
float[] samples = legacy.demodulate(iSamples, qSamples);
// float[] samples = vector.demodulate(iSamples, qSamples);
accumulator += samples[3];
}
double elapsed = System.currentTimeMillis() - start;
DecimalFormat df = new DecimalFormat("0.000");
System.out.println("Accumulator: " + accumulator);
System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds");
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator128 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator128.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator128()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator256 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator256.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator256()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator512 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator512.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator512()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -0,0 +1,88 @@
/*
* *****************************************************************************
* 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.dsp.fm;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator64 implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator64.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
//Initialize buffers to be non-null and resize once the first buffer arrives
private float[] mIBuffer = new float[1];
private float[] mQBuffer = new float[1];
public VectorFMDemodulator64()
{
}
@Override public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
}
//Copy last sample to beginning of buffer arrays
mIBuffer[0] = mIBuffer[mIBuffer.length - 1];
mQBuffer[0] = mQBuffer[mQBuffer.length - 1];
//Copy new samples to end of buffer arrays
System.arraycopy(i, 0, mIBuffer, 1, i.length);
System.arraycopy(q, 0, mQBuffer, 1, q.length);
float[] demodulated = new float[i.length];
FloatVector currentI, currentQ, previousI, previousQ, demod, demodI, demodQ;
for(int bufferPointer = 0; bufferPointer < mIBuffer.length - 1; bufferPointer += VECTOR_SPECIES.length())
{
previousI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer);
previousQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer);
currentI = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, bufferPointer + 1);
currentQ = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, bufferPointer + 1);
demodI = currentI.mul(previousI).sub(currentQ.mul(previousQ.neg()));
demodQ = currentQ.mul(previousI).add(currentI.mul(previousQ.neg()));
//Replace any zero-values in the I vector by adding Float.MIN_VALUE to side-step divide by zero errors
demodI = demodI.add(Float.MIN_VALUE, demodQ.eq(ZERO));
demod = demodQ.div(demodI).lanewise(VectorOperators.ATAN);
demod.intoArray(demodulated, bufferPointer);
}
return demodulated;
}
}

View File

@@ -1,229 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.fm;
import io.github.dsheirer.dsp.squelch.PowerSquelch;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.vector.VectorUtilities;
import jdk.incubator.vector.FloatVector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Random;
/**
* FM Demodulator for demodulating complex samples and producing demodulated floating point samples.
*
* Implements listener of source events to process runtime squelch threshold change request events
* which are forwarded to the power squelch control.
*/
public class VectorSquelchingFMDemodulator extends VectorFMDemodulator
implements ISquelchingFmDemodulator, Listener<SourceEvent>
{
private static final Logger mLog = LoggerFactory.getLogger(VectorSquelchingFMDemodulator.class);
private static final float TWO = 2.0f;
private PowerSquelch mPowerSquelch;
private boolean mSquelchChanged = false;
/**
* Creates an FM demodulator instance with a default gain of 1.0.
*/
public VectorSquelchingFMDemodulator(float alpha, float threshold, int ramp)
{
mPowerSquelch = new PowerSquelch(alpha, threshold, ramp);
}
public void reset()
{
}
/**
* Registers the listener to receive notifications of squelch change events from the power squelch.
*/
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mPowerSquelch.setSourceEventListener(listener);
}
/**
* Demodulates the complex (I/Q) sample arrays
* @param i inphase samples
* @param q quadrature samples
* @return demodulated real samples
*/
@Override
public float[] demodulate(float[] i, float[] q)
{
setSquelchChanged(false);
float[] demodulated = super.demodulate(i, q);
float[] magnitude = getMagnitude(i, q);
for(int x = 0; x < i.length; x++)
{
mPowerSquelch.process(magnitude[x]);
if(!(mPowerSquelch.isUnmuted() || mPowerSquelch.isDecay()))
{
demodulated[x] = ZERO;
}
if(mPowerSquelch.isSquelchChanged())
{
setSquelchChanged(true);
}
}
return demodulated;
}
/**
* Calculates the magnitude of the complex I/Q sample arrays using SIMD vector intrinsics.
* @param i sample array
* @param q sample array
* @return magnitude array
*/
public static float[] getMagnitude(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
float[] magnitudes = new float[i.length];
FloatVector iVector, qVector;
for(int samplePointer = 0; samplePointer < i.length; samplePointer += VECTOR_SPECIES.length())
{
iVector = FloatVector.fromArray(VECTOR_SPECIES, i, samplePointer);
qVector = FloatVector.fromArray(VECTOR_SPECIES, q, samplePointer);
iVector.pow(TWO).add(qVector.pow(TWO)).intoArray(magnitudes, samplePointer);
}
return magnitudes;
}
/**
* Sets the threshold for squelch control
* @param threshold (dB)
*/
public void setSquelchThreshold(double threshold)
{
mPowerSquelch.setSquelchThreshold(threshold);
}
/**
* Indicates if the squelch state has changed during the processing of buffer(s)
*/
public boolean isSquelchChanged()
{
return mSquelchChanged;
}
/**
* Sets or resets the squelch changed flag.
*/
private void setSquelchChanged(boolean changed)
{
mSquelchChanged = changed;
}
/**
* Indicates if the squelch state is currently muted
*/
public boolean isMuted()
{
return mPowerSquelch.isMuted();
}
@Override
public void receive(SourceEvent sourceEvent)
{
//Only forward squelch threshold change request events
if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CHANGE_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
else if(sourceEvent.getEvent() == SourceEvent.Event.REQUEST_CURRENT_SQUELCH_THRESHOLD)
{
mPowerSquelch.receive(sourceEvent);
}
}
public static void main(String[] args)
{
int iterations = 3_000_000;
int sampleSize = 2048;
Random random = new Random();
float[] iSamples = new float[sampleSize];
float[] qSamples = new float[sampleSize];
for(int x = 0; x < iSamples.length; x++)
{
iSamples[x] = random.nextFloat() * 2.0f - 1.0f;
qSamples[x] = random.nextFloat() * 2.0f - 1.0f;
}
float POWER_SQUELCH_ALPHA_DECAY = 0.0004f;
float POWER_SQUELCH_THRESHOLD_DB = -78.0f;
int POWER_SQUELCH_RAMP = 4;
ScalarFMDemodulator fm = new ScalarFMDemodulator();
VectorFMDemodulator vectorFM = new VectorFMDemodulator();
ScalarSquelchingFMDemodulator sqLegacy = new ScalarSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
VectorSquelchingFMDemodulator vectorSq = new VectorSquelchingFMDemodulator(POWER_SQUELCH_ALPHA_DECAY, POWER_SQUELCH_THRESHOLD_DB, POWER_SQUELCH_RAMP);
boolean validation = false;
if(validation)
{
float[] noSqFmSamples = fm.demodulate(iSamples, qSamples);
float[] vectorNoSqFmSamples = vectorFM.demodulate(iSamples, qSamples);
float[] sqFmSamples = sqLegacy.demodulate(iSamples, qSamples);
float[] vectorSqFmSamples = vectorSq.demodulate(iSamples, qSamples);
System.out.println(" NO SQ FM:" + Arrays.toString(noSqFmSamples));
System.out.println("VECTOR NO SQ FM:" + Arrays.toString(vectorNoSqFmSamples));
System.out.println(" SQ FM:" + Arrays.toString(sqFmSamples));
System.out.println(" VECTOR SQ FM:" + Arrays.toString(vectorSqFmSamples));
}
else
{
System.out.println("Test Starting ...");
long start = System.currentTimeMillis();
double accumulator = 0.0;
for(int i = 0; i < iterations; i++)
{
// float[] samples = sqLegacy.demodulate(iSamples, qSamples);
float[] samples = vectorSq.demodulate(iSamples, qSamples);
accumulator += samples[3];
}
double elapsed = System.currentTimeMillis() - start;
DecimalFormat df = new DecimalFormat("0.000");
System.out.println("Accumulator: " + accumulator);
System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds");
}
}
}

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,61 +16,64 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.fsk;
import io.github.dsheirer.bits.MessageFramer;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.dc.IIRSinglePoleDCRemovalFilter;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.sample.Listener;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Logic Trunked Radio (LTR) 300-baud FSK decoder is designed to operate against an 8 kHz FM demodulated audio stream.
* LTR decoder designed to work with 8 kHz input audio samples and decode 300-baud signalling.
*
* This decoder employs a zero-crossing timing error detector with adjustable symbol timing error gain. This decoder
* also employs a synchronization monitor that automatically adjusts symbol timing error gain according to the quantity
* of successful message sync pattern detections. This is optimized for LTR signals that periodically transmit idle
* bursts and transmit continuous message streams during voice transmissions. The synchronization monitor adjusts to an
* aggressive gain value when sync is lost and automatically reduces timing error gain as successive sync pattern
* detections occur.
* This decoder monitors the incoming sample stream looking for either a positive or negative sequence of samples where
* the energy delta across the baud period exceeds a threshold value and causes the symbol state to flip. The symbol
* state remains across successive baud periods until the next state change sequence is detected.
*
* FM demodulated sample buffers should not be filtered prior to this decoder. An internal DC-removal filter coupled
* with a low-pass filter removes any tuning offset and/or audio components above 300 Hertz. The DC removal filter is
* adjusted for a slower offset removal in order to prevent overly distorting the FSK signalling component. This mild
* adjustment may not fully compensate for mis-tuned signals and may result in missed decodes of idle bursts. However,
* the DC filter will track a mis-tuned signal to baseband quickly upon a continuous transmission.
* Valid LTR messages start with a zero symbol and transition to a positive symbol for the sync bit, this decoder
* monitors for excessively long sequences of a one symbol and forces the symbol state back to a zero.
*/
public class LTRDecoder implements Listener<float[]>, ISyncDetectListener, ISyncStateListener
public class LTRDecoder implements Listener<float[]>
{
private final static Logger mLog = LoggerFactory.getLogger(LTRDecoder.class);
public static final float SAMPLES_PER_SYMBOL = 8000.0f / 300.0f;
protected static final float COARSE_TIMING_GAIN = 1.0f / 3.0f;
protected static final float MEDIUM_TIMING_GAIN = 1.0f / 4.0f;
protected static final float FINE_TIMING_GAIN = 1.0f / 5.0f;
private static final Logger mLog = LoggerFactory.getLogger(LTRDecoder.class);
//Slope calculation sumXX value is always the same for 27 evenly spaced samples
private static final double SLOPE_CALCULATION_SUM_XX = 1637.999992057681;
private static final float BAUD_LENGTH = 8000.0f / 300.0f;
private static final float SLOPE_THRESHOLD = 0.0048f;
private static final int MAX_ONES_SEQUENCE = 18;
private static final int OVERLAP = 26;
private static final int POST_TRANSITION_SAMPLES_TO_SKIP = 26;
private static final int SLOPE_CALCULATION_LENGTH = 27;
private static final int SYMBOL_TRANSITION_IDEAL_MIN = 11;
private static final int SYMBOL_TRANSITION_IDEAL_MAX = 16;
private static float[] sLowPassFilterCoefficients;
private boolean mSymbol = false;
private float[] mResidual = new float[OVERLAP];
private float mBaudCounter = 0f;
private float mMaxSlope = 0;
private int mExcessiveOneSequenceCounter = 0;
private Listener<boolean[]> mSymbolListener;
static
{
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(8000)
.gridDensity(16)
.oddLength(true)
.passBandCutoff(300)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart(500)
.stopBandAmplitude(0.0)
.stopBandRipple(0.03) //Approximately 60 dB attenuation
.build();
.sampleRate(8000)
.gridDensity(16)
.oddLength(true)
.passBandCutoff(300)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart(500)
.stopBandAmplitude(0.0)
.stopBandRipple(0.03) //Approximately 60 dB attenuation
.build();
try
{
@@ -87,143 +90,193 @@ public class LTRDecoder implements Listener<float[]>, ISyncDetectListener, ISync
}
}
protected float mSymbolTimingGain = COARSE_TIMING_GAIN;
protected SampleBuffer mSampleBuffer;
protected ZeroCrossingErrorDetector mTimingErrorDetector = new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL);
protected SynchronizationMonitor mSynchronizationMonitor;
private IIRSinglePoleDCRemovalFilter mDCFilter = new IIRSinglePoleDCRemovalFilter(0.99999f);
private IRealFilter mLowPassFilter = FilterFactory.getRealFilter(sLowPassFilterCoefficients);
private MessageFramer mMessageFramer;
private boolean mSampleDecision;
/**
* Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder
*
* @param messageLength in symbols
* Constructs an instance
*/
public LTRDecoder(int messageLength)
public LTRDecoder()
{
this(messageLength, new SampleBuffer(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN),
new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL));
}
/**
* Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder.
*
* @param messageLength in symbols
* @param sampleBuffer to use for storing sample decisions
* @param detector for symbol timing errors
* Registers the listener to receive decoded symbols.
* @param listener to register
*/
public LTRDecoder(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector)
public void setListener(Listener<boolean[]> listener)
{
mSynchronizationMonitor = new SynchronizationMonitor(messageLength);
mSynchronizationMonitor.setListener(this);
mTimingErrorDetector = detector;
mSampleBuffer = sampleBuffer;
mSampleBuffer.setTimingGain(mSymbolTimingGain);
mSymbolListener = listener;
}
/**
* Implements the ISyncDetectedListener interface to be notified of message sync detection events.
*
* This allows the internal timing error detector to adjust symbol timing error gain levels according to the
* message synchronization state to quickly adjust to initial signal streams or to reduce gain levels once
* synchronization has been achieved.
* Processes the demodulated 8 kHz audio samples to extract the 300-baud LTR signalling and delivers the decoded
* symbol array to the registered listener.
* @param samples to demodulate.
*/
@Override
public void syncDetected(int bitErrors)
public void receive(float[] samples)
{
mSynchronizationMonitor.syncDetected(bitErrors);
}
@Override
public void syncLost(int bitsProcessed)
{
//no-op
}
/**
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
* adjusted and then each of the symbols are decoded using a simple majority decision.
*
* @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling.
*/
@Override
public void receive(float[] buffer)
{
float[] dcFiltered = mDCFilter.filter(buffer);
float[] lowPassFiltered = mLowPassFilter.filter(dcFiltered);
for(float sample : lowPassFiltered)
if(mSymbolListener != null)
{
mSampleDecision = sample > 0.0;
float[] filtered = mLowPassFilter.filter(samples);
mSampleBuffer.receive(mSampleDecision);
mTimingErrorDetector.receive(mSampleDecision);
int timingAdjust = 0;
int samplesToSkip = 0;
if(mSampleBuffer.hasSymbol())
float[] buffer = new float[filtered.length + OVERLAP];
boolean[] symbols = new boolean[(int)((filtered.length + mBaudCounter) / BAUD_LENGTH)];
int symbolPointer = 0;
float slope = 0;
try
{
if(mMessageFramer != null)
System.arraycopy(mResidual, 0, buffer, 0, mResidual.length);
System.arraycopy(filtered, 0, buffer, mResidual.length, filtered.length);
System.arraycopy(filtered, filtered.length - OVERLAP, mResidual, 0, OVERLAP);
for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer++)
{
mMessageFramer.process(mSampleBuffer.getSymbol());
//Don't calculate slope if we're within the baud period following last symbol transition
if(samplesToSkip > 0)
{
samplesToSkip--;
}
else
{
slope = calculateSlope(buffer, bufferPointer);
if(mSymbol)
{
//Loop for crest of slope at lowest point below the negative slope threshold for transition
if(slope > mMaxSlope && mMaxSlope < -SLOPE_THRESHOLD)
{
mSymbol = false;
//Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 16 samples
if(mBaudCounter < SYMBOL_TRANSITION_IDEAL_MIN)
{
timingAdjust = 1;
}
else if(mBaudCounter > SYMBOL_TRANSITION_IDEAL_MAX)
{
timingAdjust = -1;
}
else
{
timingAdjust = 0;
}
mBaudCounter += timingAdjust;
samplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
}
else if(slope < mMaxSlope)
{
mMaxSlope = slope;
}
}
else
{
//Loop for crest of slope at the highest point above the positive threshold
if(slope < mMaxSlope && mMaxSlope > SLOPE_THRESHOLD)
{
mSymbol = true;
//Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 16 samples
if(mBaudCounter < SYMBOL_TRANSITION_IDEAL_MIN)
{
timingAdjust = 1;
}
else if(mBaudCounter > SYMBOL_TRANSITION_IDEAL_MAX)
{
timingAdjust = -1;
}
else
{
timingAdjust = 0;
}
mBaudCounter += timingAdjust;
samplesToSkip = 26 + timingAdjust;
}
else if(slope > mMaxSlope)
{
mMaxSlope = slope;
}
}
}
mBaudCounter++;
if(mBaudCounter > BAUD_LENGTH)
{
//Expand symbols array length if needed to account for symbol timing adjustments
if(symbolPointer >= symbols.length)
{
symbols = Arrays.copyOf(symbols, symbols.length + 1);
}
symbols[symbolPointer++] = mSymbol;
mBaudCounter -= BAUD_LENGTH;
//Check for a continuous string of ones and flip the symbol state to zero.
if(mSymbol)
{
mExcessiveOneSequenceCounter++;
if(mExcessiveOneSequenceCounter > MAX_ONES_SEQUENCE) //Verify that this is the right threshold for a string of ones
{
mSymbol = false;
mMaxSlope = -1f;
mExcessiveOneSequenceCounter = 0;
}
}
else
{
mExcessiveOneSequenceCounter = 0;
}
}
}
}
catch(Exception e)
{
mLog.warn("Unexpected error while processing LTR samples", e);
}
mSampleBuffer.resetAndAdjust(-mTimingErrorDetector.getError());
mSynchronizationMonitor.increment();
//Truncate the symbols array if we didn't fill it completely, due to symbol timing adjustments.
if(symbolPointer != symbols.length)
{
mSymbolListener.receive(Arrays.copyOf(symbols, symbolPointer));
}
else
{
mSymbolListener.receive(symbols);
}
}
}
/**
* Registers a listener to receive decoded LTR symbols.
*
* @param messageFramer to receive symbols.
* Calculates the slope of the 27 samples in the values array starting at the specified offset.
* @param samples to load from
* @param offset to the sample start for the slope calculation
*/
public void setMessageFramer(MessageFramer messageFramer)
public float calculateSlope(float[] samples, int offset)
{
mMessageFramer = messageFramer;
}
/** mean of accumulated x & y values, used in updating formulas */
double xbar = 0;
double ybar = samples[offset];
/**
* Removes the symbol listener from receiving decoded LTR symbols.
*/
public void removeListener()
{
mMessageFramer = null;
}
double sumXY = 0;
double fact1, dx, dy;
/**
* Implements the ISyncStateListener interface to receive synchronization state events and adjust the symbol timing
* error gain levels on the internal timing error detector.
*
* Gain levels are defined relative to a unit gain of 1.0 over a number of symbol periods. LTR uses an initial
* ramp-up symbol reversal pattern of 0101 which provides three zero crossing detection opportunities for the timing
* error detector, therefore we react to a COARSE gain state to average timing error over 3 symbols.
*
* @param syncState from an external synchronization state monitor
*/
@Override
public void setSyncState(SyncState syncState)
{
switch(syncState)
for(int x = 1; x < SLOPE_CALCULATION_LENGTH; x++)
{
case FINE:
mSymbolTimingGain = FINE_TIMING_GAIN;
break;
case MEDIUM:
mSymbolTimingGain = MEDIUM_TIMING_GAIN;
break;
case COARSE:
mSymbolTimingGain = COARSE_TIMING_GAIN;
break;
default:
throw new IllegalArgumentException("Unrecognized sync state level: " + syncState.name());
fact1 = 1.0f + x;
dx = x - xbar;
dy = samples[offset + x] - ybar;
sumXY += dx * dy * (x / (1.0f + x));
xbar += dx / fact1;
ybar += dy / fact1;
}
mSampleBuffer.setTimingGain(mSymbolTimingGain);
return (float)(sumXY / SLOPE_CALCULATION_SUM_XX);
}
}

View File

@@ -1,40 +1,30 @@
/*******************************************************************************
* 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.dsp.fsk;
public class LTRDecoderInstrumented extends LTRDecoder
{
/**
* Instrumented extension to LTRDecoder
*
* @param messageLength in symbols
*/
public LTRDecoderInstrumented(int messageLength)
public LTRDecoderInstrumented()
{
super(messageLength, new SampleBufferInstrumented(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN),
new ZeroCrossingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL));
}
public SampleBufferInstrumented getSampleBuffer()
{
return (SampleBufferInstrumented)mSampleBuffer;
}
public ZeroCrossingErrorDetectorInstrumented getErrorDetector()
{
return (ZeroCrossingErrorDetectorInstrumented)mTimingErrorDetector;
super();
}
}

View File

@@ -0,0 +1,139 @@
/*
* *****************************************************************************
* 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.dsp.gain;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio gain that normalizes audio amplitude against an objective amplitude value where the audio amplitude values
* normally fall in the range of -1.0 to 1.0.
*
* Removes DC bias on a per audio sample buffer basis.
*
* This control is designed to work on a per-call basis by monitoring the maximum observed amplitude and adjusting the
* gain to normalize the amplitude toward the objective amplitude value. This control is designed to be reset at the
* beginning/end of each call segment so that the max observed amplitude is relevant to the current call segment.
*
* Min and max gains should be sized to constrain the applied value during periods of extended silence at the
* beginning of an audio segment or where a single amplitude spike might erroneously affect gain across the segment.
*/
public class AudioGainAndDcFilter
{
private static final Logger mLog = LoggerFactory.getLogger(AudioGainAndDcFilter.class);
private static final float MAX_AMPLITUDE = 0.95f;
private static final float GAIN_LOOP_BANDWIDTH = 0.0015f;
private float mMinGain;
private float mMaxGain;
private float mCurrentGain;
private float mObjectiveGain;
private float mObjectiveAmplitude;
private float mMaxObservedAmplitude;
/**
* Constructs an instance
* @param minGain to apply to the incoming sample stream.
* @param maxGain to apply
* @param objectiveAmplitude to achieve by adjusting gain between min and max.
*/
public AudioGainAndDcFilter(float minGain, float maxGain, float objectiveAmplitude)
{
mMinGain = minGain;
mMaxGain = maxGain;
mObjectiveAmplitude = objectiveAmplitude;
reset();
}
/**
* Resets this gain control to prepare for the next audio call/segment.
*/
public void reset()
{
mMaxObservedAmplitude = 0.0f;
mObjectiveGain = 1.0f;
mCurrentGain = 1.0f;
}
/**
* Process a buffer of audio samples and apply gain.
* @param samples to adjust.
* @return amplified audio samples
*/
public float[] process(float[] samples)
{
float currentAmplitude;
float dcAccumulator = 0.0f;
//Decay the max observed value by 10% each buffer so that an initial spike doesn't carry across all buffers
mMaxObservedAmplitude *= 0.9f;
for(float sample: samples)
{
dcAccumulator += sample;
currentAmplitude = FastMath.min(Math.abs(sample), 2.0f);
if(currentAmplitude > mMaxObservedAmplitude)
{
mMaxObservedAmplitude = currentAmplitude;
}
}
mObjectiveGain = mObjectiveAmplitude / mMaxObservedAmplitude;
if(mObjectiveGain > mMaxGain)
{
mObjectiveGain = mMaxGain;
}
else if(mObjectiveGain < mMinGain)
{
mObjectiveGain = mMinGain;
}
float dcOffset = dcAccumulator / samples.length;
float gain = mCurrentGain;
float objective = mObjectiveGain;
float[] processed = new float[samples.length];
boolean objectiveAchieved = (gain == objective);
float amplified;
for(int x = 0; x < samples.length; x++)
{
if(!objectiveAchieved)
{
gain += ((objective - gain) * GAIN_LOOP_BANDWIDTH);
if(Math.abs(objective - gain) < 0.00005f)
{
gain = objective;
objectiveAchieved = true;
}
}
amplified = (samples[x] - dcOffset) * gain;
amplified = FastMath.min(amplified, MAX_AMPLITUDE);
amplified = FastMath.max(amplified, -MAX_AMPLITUDE);
processed[x] = amplified;
}
mCurrentGain = gain;
mObjectiveGain = objective;
return processed;
}
}

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,88 +19,80 @@
package io.github.dsheirer.dsp.gain;
import io.github.dsheirer.buffer.DoubleCircularBuffer;
import io.github.dsheirer.buffer.RealCircularBuffer;
import io.github.dsheirer.buffer.FloatCircularBuffer;
import io.github.dsheirer.source.wave.RealWaveSource;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
/**
* Audio automatic gain control.
*/
public class AutomaticGainControl
{
private static final double SAMPLE_RATE = 48000;
private static final Logger mLog = LoggerFactory.getLogger(AutomaticGainControl.class);
private static final float SAMPLE_RATE = 8000;
/* Signal delay line - time delay in seconds */
// private static final double DELAY_TIME_CONSTANT = 0.015;
private static final double DELAY_TIME_CONSTANT = 0.01;
private static final float DELAY_TIME_CONSTANT = 0.015f;
/* Peak detector window - time delay in seconds */
private static final double WINDOW_TIME_CONSTANT = 0.018;
private static final float WINDOW_TIME_CONSTANT = 0.018f;
/* Attack time constants in seconds */
private static final double ATTACK_RISE_TIME_CONSTANT = 0.002;
private static final double ATTACK_FALL_TIME_CONSTANT = 0.005;
private static final double ATTACK_RISE_ALPHA = 1.0 -
FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT);
private static final double ATTACK_FALL_ALPHA = 1.0 -
FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT);
private static final float ATTACK_RISE_TIME_CONSTANT = 0.002f;
private static final float ATTACK_FALL_TIME_CONSTANT = 0.005f;
private static final float ATTACK_RISE_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_RISE_TIME_CONSTANT);
private static final float ATTACK_FALL_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / SAMPLE_RATE * ATTACK_FALL_TIME_CONSTANT);
/* AGC decay value in milliseconds (20 to 5000) */
private static final double DECAY = 200;
/* Ratio between rise and fall times of decay time constants - adjust for
* best action with SSB */
private static final double DECAY_RISEFALL_RATIO = 0.3;
private static final double DECAY_RISE_ALPHA = 1.0 -
FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO));
private static final double DECAY_FALL_ALPHA = 1.0 -
FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001));
/* Hang timer release decay time constant in seconds */
@SuppressWarnings("unused")
private static final double RELEASE_TIME_CONSTANT = 0.05;
/* Specifies the AGC Knee in dB if AGC is active (nominal range -160 to 0 dB) */
private static final double THRESHOLD = -100;
/* Limit output to about 3db of maximum */
private static final double AGC_OUT_SCALE = 0.7;
private static final float DECAY = 200.0f;
/* Ratio between rise and fall times of decay time constants - adjust for best action with SSB */
private static final float DECAY_RISEFALL_RATIO = 0.3f;
private static final float DECAY_RISE_ALPHA = 1.0f - (float)FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001 * DECAY_RISEFALL_RATIO));
private static final float DECAY_FALL_ALPHA = 1.f - (float)FastMath.exp(-1.0 / (SAMPLE_RATE * DECAY * .001));
/* Keep max input and output the same */
private static final double MAX_AMPLITUDE = 32767.0; //1.0;
private static final double MAX_MANUAL_AMPLITUDE = 32767.0; //1.0;
private static final float MAX_AMPLITUDE = 1.0f; //1.0;
private static final float MAX_MANUAL_AMPLITUDE = 1.0f; //1.0;
/* Specifies AGC manual gain in dB if AGC is not active ( 0 to 100 dB) */
private static final double MANUAL_GAIN = 0.0;
private static final double MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE *
FastMath.pow(10.0, MANUAL_GAIN / 20.0);
private static final float MANUAL_GAIN = 0.0f;
private static final float MANUAL_AGC_GAIN = MAX_MANUAL_AMPLITUDE * (float)FastMath.pow(10.0, MANUAL_GAIN / 20.0);
/* Limit output to about 3db of maximum */
private static final float AGC_OUT_SCALE = 0.95f; //0.7f;
/* Specifies dB reduction in output at knee from max output level (0 - 10dB) */
private static final double SLOPE_FACTOR = 2.0;
private static final double KNEE = THRESHOLD / 20.0;
private static final double GAIN_SLOPE = SLOPE_FACTOR / 100.0;
private static final double FIXED_GAIN = AGC_OUT_SCALE *
FastMath.pow(10.0, KNEE * (GAIN_SLOPE - 1.0));
private static final float SLOPE_FACTOR = 2.0f;
/* Specifies the threshold when the AGC kicks in (nominal range -160 to 0 dB) */
private static final float THRESHOLD = -20.0f; //-20.0f
private static final float KNEE = THRESHOLD / 20.0f; //20.0f
private static final float GAIN_SLOPE = SLOPE_FACTOR / 100.0f;
private static final float FIXED_GAIN = AGC_OUT_SCALE * (float)FastMath.pow(10.0, KNEE * (GAIN_SLOPE - 1.0));
/* Constant for calc log() so that a value of 0 magnitude = -8 */
private static final double MIN_CONSTANT = 3.2767E-4;
private static final float MIN_CONSTANT = 3.2767E-4f;
private boolean mAGCEnabled = true;
private float mPeakMagnitude = -5.0f;
private float mAttackAverage = 0.0f;
private float mDecayAverage = 0.0f;
private FloatCircularBuffer mDelayBuffer = new FloatCircularBuffer((int)(SAMPLE_RATE * DELAY_TIME_CONSTANT));
private FloatCircularBuffer mMagnitudeBuffer = new FloatCircularBuffer((int)(SAMPLE_RATE * WINDOW_TIME_CONSTANT), mPeakMagnitude);
private float mMaxGain = 0f;
private float mMinGain = 100000f;
private double mPeakMagnitude = 0.0;
private double mAttackAverage = 0.0;
private double mDecayAverage = 0.0;
private RealCircularBuffer mDelayBuffer = new RealCircularBuffer((int)(SAMPLE_RATE * DELAY_TIME_CONSTANT));
private DoubleCircularBuffer mMagnitudeBuffer = new DoubleCircularBuffer((int)(SAMPLE_RATE * WINDOW_TIME_CONSTANT));
/**
* Construct an instance
*/
public AutomaticGainControl()
{
System.out.println("Fixed gain: " + FIXED_GAIN + " Knee: " + KNEE + " Gain Slope:" + GAIN_SLOPE);
System.out.println("Manual gain: " + MANUAL_AGC_GAIN);
}
public void reset()
{
mLog.info("Previous - Min Gain: " + mMinGain + " Max Gain: " + mMaxGain);
mLog.info("Resetting - Peak:" + mPeakMagnitude + " Attack:" + mAttackAverage + " Decay:" + mDecayAverage + " Mag Max:" + mMagnitudeBuffer.max(mPeakMagnitude));
mPeakMagnitude = -8.0f;
mAttackAverage = -8.0f;
mDecayAverage = -8.0f;
mDelayBuffer.reset(0.0f);
mMagnitudeBuffer.reset(mPeakMagnitude);
}
/**
@@ -130,15 +122,14 @@ public class AutomaticGainControl
*/
public float process(float currentSample)
{
float delayedSample = mDelayBuffer.putAndGet(currentSample);
float delayedSample = mDelayBuffer.get(currentSample);
double gain = MANUAL_AGC_GAIN;
float gain = MANUAL_AGC_GAIN;
if(mAGCEnabled)
{
double currentMagnitude = FastMath.log10(FastMath.abs(currentSample) + MIN_CONSTANT) - FastMath.log10(MAX_AMPLITUDE);
double delayedMagnitude = mMagnitudeBuffer.get(currentMagnitude);
float currentMagnitude = (float)(FastMath.log10(FastMath.abs(currentSample) + MIN_CONSTANT) - FastMath.log10(MAX_AMPLITUDE));
float delayedMagnitude = mMagnitudeBuffer.get(currentMagnitude);
if(currentMagnitude > mPeakMagnitude)
{
@@ -148,29 +139,30 @@ public class AutomaticGainControl
else if(delayedMagnitude == mPeakMagnitude)
{
/* If delayed magnitude is the current peak, then find a new peak */
mPeakMagnitude = mMagnitudeBuffer.max();
mPeakMagnitude = mMagnitudeBuffer.max(-3.4845634f);
}
/* Exponential decay mode */
if(mPeakMagnitude > mAttackAverage)
{
mAttackAverage = ((1.0 - ATTACK_RISE_ALPHA) * mAttackAverage) + (ATTACK_RISE_ALPHA * mPeakMagnitude);
mAttackAverage = ((1.0f - ATTACK_RISE_ALPHA) * mAttackAverage) + (ATTACK_RISE_ALPHA * mPeakMagnitude);
}
else
{
mAttackAverage = ((1.0 - ATTACK_FALL_ALPHA) * mAttackAverage) + (ATTACK_FALL_ALPHA * mPeakMagnitude);
mAttackAverage = ((1.0f - ATTACK_FALL_ALPHA) * mAttackAverage) + (ATTACK_FALL_ALPHA * mPeakMagnitude);
}
if(mPeakMagnitude > mDecayAverage)
{
mDecayAverage = ((1.0 - DECAY_RISE_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
mDecayAverage = ((1.0f - DECAY_RISE_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
}
else
{
mDecayAverage = ((1.0 - DECAY_FALL_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
mDecayAverage = ((1.0f - DECAY_FALL_ALPHA) * mDecayAverage) + (DECAY_RISE_ALPHA * mPeakMagnitude);
}
double magnitude = FastMath.max(mAttackAverage, mDecayAverage);
float magnitude = FastMath.max(mAttackAverage, mDecayAverage);
if(magnitude < KNEE)
{
@@ -178,11 +170,24 @@ public class AutomaticGainControl
}
else
{
gain = AGC_OUT_SCALE * FastMath.pow(10.0, magnitude * (GAIN_SLOPE - 1.0));
gain = AGC_OUT_SCALE * (float)FastMath.pow(10.0, magnitude * (GAIN_SLOPE - 1.0));
}
System.out.println("Current Mag: " + currentMagnitude + " Delay Mag: " + delayedMagnitude +
" Peak:" + mPeakMagnitude + " Gain:" + gain +
" Attack Avg:" + mAttackAverage + " Decay Avg:" + mDecayAverage);
}
return (float)(delayedSample * gain);
if(gain < mMinGain)
{
mMinGain = gain;
}
if(gain > mMaxGain)
{
mMaxGain = gain;
}
return delayedSample * gain;
}
/**
@@ -202,4 +207,5 @@ public class AutomaticGainControl
{
return mAGCEnabled;
}
}

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