Compare commits

..

94 Commits

Author SHA1 Message Date
Denny Sheirer
b0a0ad4b43 Update README.md 2025-12-05 16:24:36 -05:00
Denny Sheirer
17cdd0df27 Update README.md 2025-12-05 16:07:38 -05:00
Denny Sheirer
6d23aaa06f Update README.md (#2330) 2025-12-05 15:55:46 -05:00
Denny Sheirer
933629e30d #2324 JMBE creator support for MacOS x86-64 (#2325)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-29 05:56:38 -05:00
Denny Sheirer
8a6bc03ffc 2322 Tuner Manager logs MacOS Tahoe how-to fix when LibUSB doesn't get loaded. (#2323)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-29 05:37:43 -05:00
Denny Sheirer
0c616d4531 2320 Mac OS Tahoe usb4java library update for legacy aarch64 support. (#2321)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-29 04:19:29 -05:00
AJ Collins
b472c68b80 #1892 Icecast 2.4+: Fix retries after network disruption (#2279)
* #1892 Icecast 2.4+: Fix retries after network disruption

* #1892 Icecast 2.4+: Log the connection attempt/failure once and suppress subsequent logging until reconnected
2025-11-24 03:28:33 -05:00
Denny Sheirer
f6f4b07623 #2281 Updates P25 Adjacent Site Status logging format between TSBK and AMBTC formats (#2310)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-22 06:00:00 -05:00
Denny Sheirer
de100b4662 #2304 sdrplay api now handles device failure without crash (#2309)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-22 05:44:19 -05:00
Alan Crosswell
bbde3033b7 try to get resizing to be remembered and restored. (#2269)
Per tsinmw: Simplify to use mTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS)
2025-11-22 05:24:30 -05:00
crsawyer
f6866f5408 Fixed mbe recording for P25-P2-TS2 (#2272) 2025-11-22 05:20:03 -05:00
crsawyer
d09fad56db P25 P1 MBE ESS order (#2273)
* Modified P25P1 MBE ESS order. ESS will now be populated after LDU2, not at the beginning of LDU2

* Added a version field and incremented it to '2' to signify a breaking change in the order of P25P1 ESS placement.
2025-11-22 05:18:41 -05:00
Denny Sheirer
7c06075880 #2283 Updates usb4java native libraries to 1.3.3 for MacOS Tahoe (26) fixes. (#2308)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-22 04:23:02 -05:00
Denny Sheirer
5b597e29eb #2300 Fixes Motorola unit GPS parsing, fixes P25P1 message viewer issue, and restores map center on selected coordinate in track history. (#2307)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-21 10:22:35 -05:00
Denny Sheirer
feb66831d4 #2283 Mac-OS Tahoe version 26. Updates usb4java with forked library to support multi-versioned native libraries on aarch-64 (#2306)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-21 08:41:28 -05:00
Denny Sheirer
37d47f4219 #2295 JDK25 and Gradle 9.2 and updated 3rd party libraries. (#2296)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-16 08:45:26 -05:00
Denny Sheirer
0d886c272e #1067 P25 Phase 1 C4FM/LSM decoder redesign. (#2294)
C4FM now uses SIMD differential decoding and soft sync detection.
LSM updated with soft sync detector and retains original Gardner error detector.
Adds new QPSK symbol view in the now playing channel tab.
Restores PLL error driven tuner Auto-PPM feature.
Deletes the demod viewer GUI application and all instrumented decoders
Updates DMR to send soft symbols to new symbol view in Now Playing.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-11-16 06:52:17 -05:00
Denny Sheirer
48202da9e4 #2287 Resolves Motorola P25P1 LC GPS parsing issue. (#2293)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-15 07:35:18 -05:00
Alan Crosswell
c4d9d58a5d Merge pull request #2263 from n2ygk/preserve_geometry
persist preferred spectral and control panel sizes
2025-08-04 03:17:33 -04:00
Denny Sheirer
fe5679fed7 #2265 NBFM noise squelch view saves user applied squelch changes and settings. (#2266)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-08-03 04:17:29 -04:00
Denny Sheirer
b2be38b239 #1310 NBFM decoder uses new Noise Squelch and includes new noise squelch view and control. (#2264)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-08-02 08:34:32 -04:00
Denny Sheirer
912925ee7d 2252 DMR Decoder error where LinearyInterpolator gets mu value outside 0 to 1.0 range. (#2253)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-30 03:21:08 -04:00
Denny Sheirer
d86f8bf3d9 #2250 Logback configuration issue and suppress JDK24 warnings. (#2251)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-28 07:53:27 -04:00
Denny Sheirer
88d8633958 #2234 Streaming Talkgroup Aliasing Feature (#2249)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-28 06:53:55 -04:00
Denny Sheirer
3bf3de82fc #2247 JDK 24 and Gradle 8.14.2. Also updates several 3rd party java libraries. (#2248)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-27 06:19:46 -04:00
Herb Jellinek
03a015f504 #2239 Gradle Javadoc task works correctly (#2240)
Fixes #2239 by adding the jdk.incubator.vector module, ignoring missing tags and malformed HTML.
2025-06-24 04:49:31 -04:00
Denny Sheirer
15b7923a4f #2226 DMR Decoder adds an equalizer to adjust symbol constellation balance/skew and gain to optimize symbol quadrant alignment/mapping. Reduces sync detectors from 3 to 2 and sets minimum objective channel sample rate to 19.2 kHz. (#2245)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-24 04:38:10 -04:00
Denny Sheirer
189173e289 #2219 Byte native buffers now use 8192 (vs 2096) for complex buffer sample size so that the heterodyne channel buffer size is large enough to use with the carrier offset processor that reequires 128 sample buffers. (#2220)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-04-20 08:25:24 -04:00
Denny Sheirer
2dcd994eb2 #2212 Introduce carrier offset measurement processor to monitor a 12.5 kHz channel and provide estimated carrier offset measurements that allow the tuner's frequency error monitor to correct for the average error reported from each channel. Uses a 15 dB threshold where the signal must be at least 15 dB above the noise floor before it will calculate and broadcast the estimate. Stores the updated PPM in the tuner configuration after each update to ensure the value persists across sessions. When selected in the tuner panel, updates the displayed PPM value after each PPM autocorrection. (#2218)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-04-20 06:55:43 -04:00
Denny Sheirer
afadb227db #2209 Playlist Manager double click selects the playlist (#2210)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-22 07:16:12 -04:00
tadscottsmith
5dbcd5a8ad OpenMHz Broadcast Enhancements (#2207) 2025-03-22 06:26:15 -04:00
Denny Sheirer
728a85c180 #2149 P25P1 adds L3Harris Talker GPS support (#2208)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-22 06:22:40 -04:00
Denny Sheirer
dd6ddd59e9 #2193 Removes "Log Settings" button from the channel view. This is a debug button. (#2205)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 08:02:47 -04:00
Denny Sheirer
758794fdb4 #2195 Fix P25P1 NPE when accessing AMBTC network status message data blocks. (#2204)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 07:54:47 -04:00
Denny Sheirer
49b95a9445 #2202 DMR talker aliases now show in channel activity summary. Added 'TA-' prefix to talker aliases (DMR & P25) to visually highlight what the value represents. (#2203)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 07:27:33 -04:00
Denny Sheirer
a4bcdfa769 #2127 DMR capacity plus wide area voice channel user link control messages now create a call event (#2201)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 06:46:28 -04:00
Denny Sheirer
872498badc #2166 DMR adds RS(12,9) error detection and correction on link control. Enhances DMR CRC mask manager to process CSBK and link control messages to improve handling of RAS enabled systems when the user has not opted to ignore CRC checksums. Fixes issue with CapMax voice channels not resolving to frequency. Fixes issue in playlist editor where DMR LCN description values were not updating correctly. (#2200)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 06:23:58 -04:00
Sir Christoff Von-Dryden Of The Bailey LLC
4afb74a847 Check the I2CRepeater state before changing it. (#2197)
Resolves #1923 - 'USB Error 9' preventing configuration of rtl-sdr devices
2025-03-20 02:17:31 -04:00
Denny Sheirer
96f0b7dd55 #2174 Playlist editor channels tab, frequency column sort ordering. (#2192)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-14 04:04:59 -04:00
Randy
b360950b80 Update IntegerFormatter.java for negative squelch value (#2187)
Two issues:
(1)documentation for change.getText() says it can be an empty string but never null. 
(2)Need to allow for typing a minus sign before validating min/max.
2025-03-14 03:52:49 -04:00
Denny Sheirer
5b0a1401a1 #2157 Resolves audio output getting 'stuck' when the thread is blocking on an attempt to write audio to the data line and that blocking call is not executing. Also updates previous attempt at fixing a stalled data line by closing and reopening the line, without having to recreate the data line, which was causing related issues with the gain and muting behavior. (#2191)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-13 04:09:26 -04:00
Daniel Ponte
3fa114501b Add talker alias field to rdio-scanner upload (#2186) 2025-03-11 02:53:51 -04:00
Denny Sheirer
8935835921 #2177 DMR TMS packet parsing issue (#2178)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-10 03:54:02 -04:00
Denny Sheirer
dec255d444 #2175 DMR Text Message Service (TMS) support. (#2176)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-10 03:45:16 -04:00
tadscottsmith
1cb0490985 Add Motorola Talker Alias Identifier (#2154)
* Add MotorolaTalkerAlias identifier.

* Remove duplicate call to talkerAliasManager.

* Use TalkerAlias RID and remove additional cosmetic updates.
2025-03-10 02:27:39 -04:00
Denny Sheirer
c8235ea4fc #2171 Updates OpenJDK to version 23.0.2+9 release for the build system. (#2172)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-09 09:00:29 -04:00
Randy
7d69d829b5 Fix adding available channel to wrong queue (#2169)
Available channel was added to P1 queue instead of P2 queue.
2025-03-09 07:40:38 -04:00
Randy
a46abf7da8 Cleanup of recording paths #2128 (#2168)
* Update replaceIllegalCharacters

* Add underscore after timestamp
2025-03-09 07:34:52 -04:00
modificaz
437264e272 Fix incorrect array assignment (frame_3 → frame_2) (#2159) 2025-03-09 07:29:23 -04:00
Denny Sheirer
8718d04ef5 #2162 APCO25 NID BCH decoder implementation. (#2163)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-02-27 09:09:46 -05:00
Denny Sheirer
4e367bb5fe W#2150 Adds AMBE audio tone aliasing support to DMR. (#2151)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-02-15 07:20:46 -05:00
Dennis Sheirer
71d2cb6fca sdrtrunk GitHub issues templates update. 2025-01-20 06:50:59 -05:00
Dennis Sheirer
e856de273d sdrtrunk GitHub issues templates update. 2025-01-20 06:48:04 -05:00
Denny Sheirer
ea0a9da24c #2138 DMR audio segments get chopped with every invalid link control message and remove the insert of Talkgroup 1 as a default value for conventional and direct mode channels. (#2139)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-20 06:40:27 -05:00
Denny Sheirer
b3cd7d787a #2125 Motorola Talker Alias CRC index out of bounds error. (#2126)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-07 03:44:58 -05:00
Denny Sheirer
9a13a82ed1 Update README.md (#2124)
Updates min OSX supported version.
2025-01-04 07:07:43 -05:00
Denny Sheirer
9b71508d49 #2122 Removes extraneous logging from DMR processing. (#2123)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-01 10:05:20 -05:00
Denny Sheirer
694ea83a80 #2120 DMR Decoder resolves index out of bounds error and also updates Hamming 16/17 decoders to correctly identify multi-bit errors when the hamming parity bit is the syndrome. (#2121)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-01 09:51:41 -05:00
Denny Sheirer
70fb74b787 #2117 DMR simplex transmissions now frame correctly and added additional case for FLC BPTC error correction. (#2118)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-25 08:14:48 -05:00
Denny Sheirer
9ffa02e2e5 #1633 DMR Decoder redesign. Uses new DQPSK vector/scalar demodulator implementations and soft sync detectors for more accurate detections and symbol timing and symbol period calculations. Includes two new calibrations with scalar and vector implementations for DQPSK demodulator and Interpolator. Enhances DMR error detection and correction algorithms, specifically the block product turbo code algorithms. (#2115)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-24 06:53:58 -05:00
ilyacodes
11abb87177 Add Motorola Failsoft LC opcode $02 (#2104)
This is broadcast on channels when a site is in failsoft operation (site controller has failed or too few channels are functional to operate in trunking mode).
2024-12-06 12:29:05 -05:00
Denny Sheirer
49cf92c7ab #2105 P25 P1/2 Motorola Talker Alias CRC Check (#2106)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-06 04:33:14 -05:00
Denny Sheirer
4cf51405ea #2102 Log Unknown Tuners From SDRPlay API (#2102)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-03 17:16:37 -05:00
Dennis Sheirer
db83d5121c Version 0.6.1 Final 2024-12-03 16:29:13 -05:00
ilyacodes
d61d9c07b0 Add Motorola OTA alias decoding (#2098) 2024-12-02 18:31:37 -05:00
Denny Sheirer
5c9c3fa503 #2089 Motorola P25P2 TDMA data channel support (#2090)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-12 02:22:22 -05:00
Denny Sheirer
552d3a6ce9 #2086 Audio Playback Manager can leak memory if audio queue processing thread is never started. (#2087)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-08 03:53:21 -05:00
Denny Sheirer
3bf425f94a #2084 Updates usb4java native library for windows aarch64 (#2085)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-06 03:59:14 -05:00
Denny Sheirer
660a5acd2f #2081 Windows aarch64 libusb support. (#2083)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-05 03:56:19 -05:00
Denny Sheirer
551277f399 #2078 Broadcastify feed verbose logging now includes logging of audio and metadata (#2080)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-03 03:20:17 -05:00
Dennis Sheirer
a12040be5c Revert "#2076 DMR CapMax Network and Site vales off by one. (#2077)"
This reverts commit e4be7dc5f4.
2024-11-02 06:58:23 -04:00
Denny Sheirer
e4be7dc5f4 #2076 DMR CapMax Network and Site vales off by one. (#2077)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-02 06:44:20 -04:00
Denny Sheirer
b615c1e16a #2074 DMR CapMax LRRP GPS positions sent to map now contain channel identifiers (alias list and system name). (#2075)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-02 06:12:38 -04:00
Denny Sheirer
912af4e3e6 #2071 Version 0.6.1 Beta 3 (#2072)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-01 05:46:42 -04:00
Denny Sheirer
df0d0468c8 #2055 MultiFrequencyTunerChannelSource Null Pointer Error On Channel Source Shutdown - different solution to resolve possible memory leak with previous solution. Also fixes P25 issue where a traffic channel that doesn't get started is returned to the traffic channel pool. (#2070)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-11-01 05:32:10 -04:00
Denny Sheirer
7509245751 #2068 P25 traffic channel manager ignores traffic channel requests if the frequency value is zero (#2069)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 05:53:42 -04:00
Denny Sheirer
02e236202c #2066 P25 Phase 1 frequency band link control messages are now checked for valid CRC before being used. (#2067)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 05:29:51 -04:00
Denny Sheirer
4b1d294211 #2064 Moto talker GPS event TO/FROM identifiers. (#2065)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 05:07:42 -04:00
Denny Sheirer
40840dbd5c #2062 Verbose logging option for Broadcastify Feed to troubleshoot streaming connection issues. (#2063)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-31 04:11:45 -04:00
Denny Sheirer
786ae9369b #1992 Traffic channels stuck in teardown. (#2061)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-30 03:57:42 -04:00
Denny Sheirer
b7bbf10415 #2055 Multi-Frequency channel source with channel rotation monitor startup can get ahead of the tuner channel source and cause an NPE. (#2056)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-28 03:30:37 -04:00
Denny Sheirer
486ac01416 #1867 Event logger ensures from identifier is the radio ID and not the system transmitted radio alias. (#2050)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 09:31:51 -04:00
Denny Sheirer
9e01cc48a5 #2034 Map plotted elements now include date in the location timestamp. (#2049)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 09:08:41 -04:00
Denny Sheirer
37996d0477 #2040 Reset AMBE/IMBE audio codec after each call to prevent carryover of last audio frame from previous call. (#2048)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 08:34:27 -04:00
Denny Sheirer
a15ff9040b #2037 JavaFX Windows Can't Be Shrunk. Reverts changes made under #1619. (#2047)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 08:22:45 -04:00
Denny Sheirer
b3cd3f431f #2045 P25 Phase 1 Data channels are now ignored if the channel config calls for it. (#2046)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-27 05:29:25 -04:00
Denny Sheirer
649d1f650a #2043 DMR Tier 3 Capacity Max talkgroups were parsing incorrectly. (#2044)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-26 16:35:59 -04:00
Denny Sheirer
215e4064fa #2039 P25P2 bogus talkgroup and radio values showing in calls. Corrects message length error that was causing mis-aligned mac message parsing that contained bad/errant talkgroup and radio values to be included in the call metadata. Adds support for a new Moto feature discovery that appears to be announcing talkgroup member radios that are active on/tuned to the traffic channel. (#2042)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-26 09:18:35 -04:00
Denny Sheirer
66a0b2543b #1060 Adds Alias List delete and rename operations to playlist editor (#2038)
Original code changes implemented by @whelgeson

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-21 05:34:06 -04:00
Denny Sheirer
378841b408 #2028 Adds support for Windows ARM/aarch64 Operating System (#2029)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-19 06:11:54 -04:00
Denny Sheirer
e28d726182 #2035 P25P1 Harris Patched Talkgroup Values Incorrect (#2036)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-19 04:43:15 -04:00
Denny Sheirer
71bc27e3d9 #2032 Audio output suppresses logging after 5 times when app can't get gain or mute controls for current audio output. (#2033)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-18 03:05:21 -04:00
Denny Sheirer
bcfb2700cb #2030 Playlist Editor double-click channel row to auto-start channel. (#2031)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-18 02:43:55 -04:00
Denny Sheirer
2d9491ac93 #2025 P25P1 audio module null codec error. (#2027)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-18 01:33:23 -04:00
480 changed files with 25030 additions and 14563 deletions

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Support and Questions - How do I XXX? Does it support XXX?
url: https://github.com/DSheirer/sdrtrunk/wiki/Support
about: Please ask your user questions here -- DO NOT create an issue if you have a user question please!!

View File

@@ -1,16 +0,0 @@
---
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

@@ -22,10 +22,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 23
- name: Set up JDK 25
uses: actions/setup-java@v3
with:
java-version: '23'
java-version: '25'
distribution: 'liberica'
java-package: 'jdk+fx'
- name: Build with Gradle

View File

@@ -15,10 +15,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 23
- name: Set up JDK 25
uses: actions/setup-java@v3
with:
java-version: '23'
java-version: '25'
distribution: 'liberica'
java-package: 'jdk+fx'

View File

@@ -2,18 +2,24 @@
<dictionary name="denny">
<words>
<w>bahroo</w>
<w>baseband</w>
<w>basebanded</w>
<w>broadcastify</w>
<w>cach</w>
<w>correlator</w>
<w>deinterleave</w>
<w>descrambled</w>
<w>dibit</w>
<w>dinf</w>
<w>dqpsk</w>
<w>esnid</w>
<w>fleetsync</w>
<w>heterdyned</w>
<w>heterodynes</w>
<w>hytera</w>
<w>icecast</w>
<w>ident</w>
<w>lcns</w>
<w>nutall</w>
<w>overflowable</w>
<w>polyphase</w>

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_23_PREVIEW" project-jdk-name="liberica-23" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_25_PREVIEW" project-jdk-name="liberica-25" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,6 +1,24 @@
![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)
# MacOS Tahoe 26.1 Users - Attention:
Changes to USB support in Tahoe version 26.x cause sdrtrunk to fail to launch. Do the following to install the latest libusb and create a symbolic link and then use the nightly build which includes an updated usb4java native library for Tahoe with ARM processor. There may still be issue(s) with MacOS accessing your USB SDR tuners.
```
brew install libusb --HEAD
cd /opt
sudo mkdir local
cd local
sudo mkdir lib
```
Next, find where brew installed the libusb library, for example: ```/opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib``` Note: the folder "HEAD-9ceaa52" is the version stamp for HEAD when you installed from it.
Finally, create a symbolic link from the installed library to the place where usb4java is expecting to find libusb (/opt/local/lib/libusb-1.0.0.dylib)
```
sudo ln -s /opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib /opt/local/lib/libusb-1.0.0.dylib
```
# sdrtrunk
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
@@ -28,6 +46,6 @@ playlist(s) before you use the nightly builds.** Note: the nightly release is u
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)
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac (64-bit, 12.x or higher)
* **CPU:** 4-core
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -47,21 +47,21 @@ plugins {
id 'application'
id 'java'
id 'idea'
id 'org.beryx.runtime' version '1.12.7'
id 'org.beryx.runtime' version '2.0.1'
}
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
maven { url = "https://jitpack.io" }
}
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 23 is required for this version of the Project Panama preview/incubator feature
//Java 24 is required for this version of the Project Panama preview/incubator feature
java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
languageVersion = JavaLanguageVersion.of(25)
}
}
@@ -77,11 +77,11 @@ test {
dependencies {
// JUnit Tests
testImplementation(platform("org.junit:junit-bom:5.11.0"))
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
testImplementation(platform("org.junit:junit-bom:6.0.1"))
testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.1'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
//Jitpack imports
implementation 'com.github.dnault:libresample4j:master'
@@ -89,37 +89,37 @@ dependencies {
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
//mavenCentral/jcenter imports
implementation 'ch.qos.logback:logback-classic:1.4.12'
implementation 'ch.qos.logback:logback-core:1.4.14'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
implementation 'com.fazecast:jSerialComm:2.9.3'
implementation 'ch.qos.logback:logback-classic:1.5.21'
implementation 'ch.qos.logback:logback-core:1.5.21'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.20.1'
implementation 'com.fazecast:jSerialComm:2.11.4'
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation 'com.github.wendykierp:JTransforms:3.1'
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.google.guava:guava:33.3.0-jre'
implementation 'com.google.code.gson:gson:2.13.2'
implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:11.0'
implementation 'com.miglayout:miglayout-swing:11.4.2'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.11.0'
implementation 'commons-io:commons-io:2.21.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.3' //OSX & Windows aarch64 native libs
implementation 'io.github.dsheirer:usb4java:1.3.5' //Fork with Loader support for OSX Multi-Version
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
implementation 'org.apache.commons:commons-compress:1.26.0'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-compress:1.28.0'
implementation 'org.apache.commons:commons-lang3:3.19.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-csv:1.9.0'
implementation 'org.apache.mina:mina-core:2.2.1'
implementation 'org.apache.mina:mina-http:2.2.1'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'org.apache.commons:commons-csv:1.14.1'
implementation 'org.apache.mina:mina-core:2.2.4'
implementation 'org.apache.mina:mina-http:2.2.4'
implementation 'org.controlsfx:controlsfx:11.2.2'
implementation 'org.rauschig:jarchivelib:1.2.0'
implementation 'org.slf4j:slf4j-api:2.0.5'
implementation 'org.slf4j:slf4j-api:2.0.17'
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
@@ -142,6 +142,9 @@ def jvmArgsWindows =
'--add-modules=jdk.incubator.vector', //Project Panama Vector API is still in incubator
'--enable-preview', //Project Panama Vector API is still in incubator
'--enable-native-access=ALL-UNNAMED', //Suppress unsafe warnings for FFM API Foreign Memory calls
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
'-XX:+UseCompactObjectHeaders', //JDK25 - use compact object headers
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //SDRPlay API library path
/**
@@ -151,10 +154,13 @@ 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
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
'-XX:+UseCompactObjectHeaders', //JDK25 - use compact object headers
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
'--enable-native-access=ALL-UNNAMED'] //SDRPlay API library path
application {
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
mainClass = "io.github.dsheirer.gui.SDRTrunk"
if(os.isWindows()) {
applicationDefaultJvmArgs = jvmArgsWindows
@@ -164,6 +170,15 @@ application {
}
}
test {
if(os.isWindows()) {
jvmArgs = jvmArgsWindows
}
else {
jvmArgs = jvmArgsLinux
}
}
jar {
manifest {
attributes (
@@ -184,6 +199,11 @@ idea {
}
}
javadoc {
options.addStringOption('-add-modules', 'jdk.incubator.vector')
options.addStringOption('Xdoclint:none', '-quiet')
}
/**
* Runtime target OS and CPU image names. We append version here so that version details are at end of zip filename.
*/
@@ -192,28 +212,31 @@ String targetLinuxX86_64 = 'linux-x86_64-v' + version
String targetOsxAarch64 = 'osx-aarch64-v' + version
String targetOsxX86_64 = 'osx-x86_64-v' + version
String targetWindowsX86_64 = 'windows-x86_64-v' + version
String targetWindowsAarch64 = 'windows-aarch64-v' + version
/**
* Optional pre-downloaded Java Development Kit (JDK) for each target OS and CPU. These locations are hard-coded to the
* 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-22.0.2-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-22.0.2-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-22.0.2-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-22.0.2-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-22.0.2-full'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-25.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-25.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-25.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-25.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-25.0.1-full'
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-25.0.1-full'
/**
* Download URLs to download the JDK as part of the gradle build packaging process
*/
def jdk_download_base = "https://download.bell-sw.com/java/23+38/bellsoft-jdk23+38-"
def jdk_download_base = "https://download.bell-sw.com/java/25.0.1+11/bellsoft-jdk25.0.1+11-"
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"
def jdk_download_windows_aarch64 = jdk_download_base + "windows-aarch64-full.zip"
/**
* Configures the runtime zip task with additional options/settings.
@@ -256,7 +279,12 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
if(os.isWindows()) {
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
if(arch.equals("amd64")) {
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
}
else if(arch.equals("aarch64")) {
rt.extension.targetPlatform(targetWindowsAarch64, javaHome)
}
configure(rt, jvmArgsWindows)
}
else if(os.isLinux())
@@ -292,6 +320,16 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
*/
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
//Windows aarch64
if(file(jdk_windows_aarch64).exists()) {
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, [])
}
else {
rt.extension.targetPlatform(targetWindowsAarch64) {
jdkHome = jdkDownload(jdk_download_windows_aarch64)
}
}
//Windows x86-64
if(file(jdk_windows_x86_64).exists()) {
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
}
@@ -351,5 +389,5 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
}
}
configure(rt, jvmArgsLinux);
configure(rt, jvmArgsLinux)
}

View File

@@ -28,4 +28,4 @@
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
projectVersion=0.6.1-beta-2
projectVersion=0.6.2-beta-1

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#
# ******************************************************************************
# Copyright (C) 2014-2024 Dennis Sheirer
# Copyright (C) 2014-2025 Dennis Sheirer
#
# This 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-8.10-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

25
gradlew vendored
View File

@@ -2,7 +2,7 @@
#
# ******************************************************************************
# Copyright (C) 2014-2024 Dennis Sheirer
# Copyright (C) 2014-2025 Dennis Sheirer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -58,7 +58,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -86,7 +86,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +115,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -147,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -155,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -204,16 +205,16 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

26
gradlew.bat vendored
View File

@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
@@ -57,22 +59,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.alias;
@@ -30,13 +27,22 @@ import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.record.Record;
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
@@ -46,13 +52,6 @@ import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Alias provides an aliasing (e.g. name, color, etc) container that is linked to multiple alias identifiers and
* provides a corresponding set of actions related to the alias.
@@ -74,6 +73,7 @@ public class Alias
private StringProperty mName = new SimpleStringProperty();
private ObservableList<AliasID> mAliasIDs = FXCollections.observableArrayList();
private ObservableList<AliasAction> mAliasActions = FXCollections.observableArrayList();
private ObjectProperty<StreamAsTalkgroup> mStreamTalkgroupAlias = new SimpleObjectProperty<>();
/**
* Constructs an instance and sets the specified name.
@@ -180,6 +180,12 @@ public class Alias
return mColor;
}
@JsonIgnore
public ObjectProperty streamTalkgroupAliasProperty()
{
return mStreamTalkgroupAlias;
}
/**
* Icon name property
*/
@@ -229,6 +235,24 @@ public class Alias
mName.set(name);
}
/**
* Alias name
*/
@JacksonXmlProperty(isAttribute = true, localName = "stream_talkgroup_alias")
public StreamAsTalkgroup getStreamTalkgroupAlias()
{
return mStreamTalkgroupAlias.get();
}
/**
* Sets the stream as talkgroup value that will be used in the TO field for streamed audio calls.
* @param streamTalkgroupAlias with a talkgroup value.
*/
public void setStreamTalkgroupAlias(StreamAsTalkgroup streamTalkgroupAlias)
{
mStreamTalkgroupAlias.set(streamTalkgroupAlias);
}
/**
* Alias list name for the alias list that this alias belongs to
*/
@@ -710,6 +734,7 @@ public class Alias
{
return (Alias a) -> new Observable[] {a.recordableProperty(), a.streamableProperty(), a.colorProperty(),
a.aliasListNameProperty(), a.groupProperty(), a.iconNameProperty(), a.nameProperty(), a.aliasIds(),
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty()};
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty(), a.priorityProperty(),
a.streamTalkgroupAliasProperty()};
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -63,6 +63,69 @@ public class AliasModel
return mAliasListNames;
}
/**
* Clears and reloads the list of alias list names from the current set of aliases.
*/
public void refreshAliasListNames()
{
mAliasListNames.clear();
for(Alias alias : mAliases)
{
String aliasListName = alias.getAliasListName();
if(aliasListName != null && !aliasListName.isEmpty() && !mAliasListNames.contains(aliasListName))
{
mAliasListNames.add(aliasListName);
}
}
}
/**
* Renames the alias list across the set of aliases.
* @param oldName currently used by the alias
* @param newName to apply to the alias
*/
public void renameAliasList(String oldName, String newName)
{
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
{
return;
}
mAliases.stream().filter(alias -> alias.getAliasListName().equals(oldName)).forEach(alias -> alias.setAliasListName(newName));
}
/**
* Deletes any aliases that have the alias list name
* @param aliasListName to delete
*/
public void deleteAliasList(String aliasListName)
{
if(aliasListName == null || aliasListName.isEmpty())
{
return;
}
mAliases.removeIf(alias -> alias.getAliasListName().equals(aliasListName));
mAliasListMap.remove(aliasListName);
}
/**
* Adds the list of alias list names to the currently managed set of alias list names.
* @param aliasListNames to add
*/
public void addAliasListNames(List<String> aliasListNames)
{
for(String aliasListName : aliasListNames)
{
if(!mAliasListNames.contains(aliasListName))
{
mAliasListNames.add(aliasListName);
}
}
}
/**
* Unmodifiable list of all aliases currently in the model
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,6 +42,7 @@ 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.StreamAsTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.alias.id.tone.TonesID;
@@ -70,6 +71,7 @@ import javafx.util.Callback;
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
@JsonSubTypes.Type(value = Record.class, name = "record"),
@JsonSubTypes.Type(value = SiteID.class, name = "siteID"),
@JsonSubTypes.Type(value = StreamAsTalkgroup.class, name = "streamAsTalkgroup"),
@JsonSubTypes.Type(value = Talkgroup.class, name = "talkgroup"),
@JsonSubTypes.Type(value = TalkgroupRange.class, name = "talkgroupRange"),
@JsonSubTypes.Type(value = TonesID.class, name = "tones"),

View File

@@ -0,0 +1,40 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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 io.github.dsheirer.protocol.Protocol;
/**
* Streaming talkgroup alias. For streamed audio metadata, this value is used to replace the decoded talkgroup or TO
* value with an alias value when there are talkgroup collisions resulting from streaming multiple channels to the
* same stream. This is common in P25 conventional repeater setups where each radio channel is using talkgroup 1.
*/
public class StreamAsTalkgroup extends Talkgroup
{
public StreamAsTalkgroup(int talkgroup)
{
super(Protocol.UNKNOWN, talkgroup);
}
public StreamAsTalkgroup()
{
//No arg JAXB constructor
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,6 +20,8 @@
package io.github.dsheirer.audio.broadcast.broadcastify;
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;
@@ -32,6 +34,7 @@ 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.AliasListConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
@@ -45,6 +48,7 @@ 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;
@@ -76,6 +80,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
final private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
@@ -86,6 +91,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(config);
mAliasModel = aliasModel;
}
/**
@@ -443,10 +449,34 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
/**
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*/
private static String getTo(AudioRecording audioRecording)
private String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
//Alias the TO value when the user specifies a 'Stream As Talkgroup'
if(identifier != null)
{
AliasListConfigurationIdentifier config = audioRecording.getIdentifierCollection().getAliasListConfiguration();
if(config != null)
{
AliasList aliasList = mAliasModel.getAliasList(config.getValue());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
for(Alias a: aliases)
{
if(a.getStreamTalkgroupAlias() != null)
{
return String.valueOf(a.getStreamTalkgroupAlias().getValue());
}
}
}
}
}
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return format(patchGroupIdentifier);

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 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.broadcast.broadcastify;
@@ -35,6 +32,7 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyFeedConfiguration.class);
private int mFeedID;
private boolean mVerboseLogging = false;
public BroadcastifyFeedConfiguration()
{
@@ -123,4 +121,15 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
{
mFeedID = feedID;
}
@JacksonXmlProperty(isAttribute = true, localName = "verbose_logging")
public boolean isVerboseLogging()
{
return mVerboseLogging;
}
public void setVerboseLogging(boolean verboseLogging)
{
mVerboseLogging = verboseLogging;
}
}

View File

@@ -52,6 +52,7 @@ import org.slf4j.LoggerFactory;
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; // 5 seconds
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private static final String HTTP_1_0_OK_HEX_DUMP = "48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B";
@@ -61,6 +62,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
private AtomicBoolean mConnectionFailureLogged = new AtomicBoolean(false);
/**
* Creates an Icecast 2.4.x compatible broadcaster using HTTP 1.1 protocol. This broadcaster is
@@ -141,7 +143,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
mSocketConnector.setConnectTimeoutMillis(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
mSocketConnector.setConnectTimeoutCheckInterval(1000);
// LoggingFilter loggingFilter = new LoggingFilter(IcecastHTTPAudioBroadcaster.class);
// loggingFilter.setMessageReceivedLogLevel(LogLevel.DEBUG);
@@ -160,37 +163,58 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
try {
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
mStreamingSession = future.getSession();
}
catch(RuntimeIoException rie)
{
boolean connected;
try {
connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
if (mConnectionFailureLogged.compareAndSet(false, true)) {
mLog.debug("HTTP connect attempt interrupted");
}
setBroadcastState(BroadcastState.DISCONNECTED);
mLastConnectionAttempt = System.currentTimeMillis();
future.cancel();
disconnect();
return;
}
if (connected && future.isConnected()) {
mStreamingSession = future.getSession();
mConnecting.set(false);
mConnectionFailureLogged.set(false);
return;
} else {
if (mConnectionFailureLogged.compareAndSet(false, true)) {
mLog.debug("HTTP connect attempt timed out ({} ms) or not connected",
CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
}
setBroadcastState(BroadcastState.DISCONNECTED);
mLastConnectionAttempt = System.currentTimeMillis();
future.cancel();
disconnect();
}
} catch (RuntimeIoException rie) {
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLog.debug("Failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLog.debug("Failed to connect - no exception is available");
}
setBroadcastState(BroadcastState.DISCONNECTED);
if (mConnectionFailureLogged.compareAndSet(false, true)) {
if (throwableCause != null) {
mLog.debug("Failed to connect", rie);
} else {
mLog.debug("Failed to connect - no exception is available");
}
}
mLastConnectionAttempt = System.currentTimeMillis();
disconnect();
} finally {
if (!connected()) {
mConnecting.set(false);
}
}
mConnecting.set(false);
}
};
@@ -210,6 +234,11 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
mStreamingSession.closeNow();
}
mLastConnectionAttempt = System.currentTimeMillis();
if(!getBroadcastState().isErrorState()) {
setBroadcastState(BroadcastState.DISCONNECTED);
}
}
/**
@@ -288,6 +317,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
public void sessionClosed(IoSession session) throws Exception
{
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
@@ -323,6 +353,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
{
setBroadcastState(BroadcastState.CONNECTED);
mConnectionFailureLogged.set(false);
}
else
{
@@ -405,6 +436,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
case SUCCESS_OK:
setBroadcastState(BroadcastState.CONNECTED);
mConnecting.set(false);
mConnectionFailureLogged.set(false);
break;
case CLIENT_ERROR_UNAUTHORIZED:
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.broadcast.icecast;
@@ -25,21 +22,16 @@ 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.icecast.IcecastConfiguration;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class IcecastMetadata
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadata.class);
@@ -73,13 +65,23 @@ public class IcecastMetadata
if(to != null)
{
sb.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
if(aliases != null && !aliases.isEmpty())
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
}
else
{
sb.append("TO:").append(to);
if(!aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
}
else

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.broadcast.icecast;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3AudioConverter;
@@ -39,6 +40,8 @@ import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.filter.logging.LogLevel;
import org.apache.mina.filter.logging.LoggingFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,6 +59,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
private IoSession mStreamingSession = null;
private long mLastConnectionAttempt = 0;
private boolean mVerboseLogging = false;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
@@ -73,6 +77,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
if(configuration instanceof BroadcastifyFeedConfiguration broadcastify)
{
mVerboseLogging = broadcastify.isVerboseLogging();
}
}
/**
@@ -85,7 +94,6 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
if(mInlineActive)
{
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
if (mInlineRemaining == -1)
{
mInlineRemaining = mInlineInterval;
@@ -96,16 +104,34 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
byte[] chunk = Arrays.copyOfRange(audio, audioOffset, Math.min(audioOffset + mInlineRemaining, audio.length));
mInlineRemaining -= chunk.length;
audioOffset += chunk.length;
if(mVerboseLogging)
{
mLog.info("Broadcasting audio data - sending [" + chunk.length + "/" + audio.length + "] bytes");
}
mStreamingSession.write(chunk);
if(mInlineRemaining == 0)
{
mInlineRemaining = mInlineInterval;
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
if(mVerboseLogging)
{
mLog.info("Sending inline metadata: " + new String(metadata));
}
mStreamingSession.write(metadata);
}
}
}
else
{
if(mVerboseLogging)
{
mLog.info("Broadcasting audio data [" + audio.length + "] bytes");
}
mStreamingSession.write(audio);
}
}
@@ -132,9 +158,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mSocketConnector = new NioSocketConnector();
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
if(mVerboseLogging)
{
LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
}
mSocketConnector.getFilterChain().addLast("codec",
new ProtocolCodecFilter(new IcecastCodecFactory()));
@@ -148,6 +177,10 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
public void run()
{
if(mVerboseLogging)
{
mLog.info("Attempting connection ...");
}
setBroadcastState(BroadcastState.CONNECTING);
try
@@ -156,19 +189,42 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
if(mVerboseLogging)
{
mLog.info("Socket created - asynchronous connect requested - entering wait period");
}
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
if(connected)
{
if(mVerboseLogging)
{
mLog.info("Connected.");
}
mStreamingSession = future.getSession();
mConnecting.set(false);
return;
}
else
{
if(mVerboseLogging)
{
mLog.info("Not Connected. Connection attempt timeout [" + CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS + "ms] exceeded");
}
}
}
catch(RuntimeIoException rioe)
{
if(rioe.getCause() instanceof SocketException)
{
if(mVerboseLogging)
{
mLog.info("Socket error. This usually indicates sdrtrunk can't reach the server " +
"address over the current network connection. Setting state to " +
"NETWORK UNAVAILABLE", rioe);
}
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
mConnecting.set(false);
return;
@@ -176,12 +232,22 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
}
catch(UnresolvedAddressException uae)
{
if(mVerboseLogging)
{
mLog.info("Unresolved Address error. This means the domain name services can't resolve " +
"the server URL to an IP address. Setting state to NETWORK UNAVAILABLE", uae);
}
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
mConnecting.set(false);
return;
}
catch(Exception e)
{
if(mVerboseLogging)
{
mLog.info("Unknown error. An error occurred while attempting to connect to the server.", e);
}
mLog.error("Error", e);
//Disregard ... we'll disconnect and try again
}
@@ -190,6 +256,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mLog.error("Throwable error caught", t);
}
if(mVerboseLogging)
{
mLog.info("Starting disconnect sequence since an error occurred while trying to connect.");
}
disconnect();
mConnecting.set(false);
}
@@ -209,6 +280,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
if(connected() && mStreamingSession != null)
{
if(mVerboseLogging)
{
mLog.info("Routine disconnect requested from a connected state with a non-null streaming session");
}
mStreamingSession.closeNow();
}
else
@@ -217,6 +293,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
//want to preserve the error state that got us here, so the user can see it.
if(!getBroadcastState().isErrorState())
{
if(mVerboseLogging)
{
mLog.info("Disconnect requested - previous non-error state was [" + getBroadcastState() +
"] - changing state to DISCONNECTED");
}
setBroadcastState(BroadcastState.DISCONNECTED);
}
@@ -282,12 +364,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mInlineActive = false;
}
if(mVerboseLogging)
{
mLog.info("Session opened. Sending: " + sb);
}
session.write(sb.toString());
}
@Override
public void sessionClosed(IoSession session) throws Exception
{
if(mVerboseLogging)
{
mLog.info("Session closed. Setting connecting flag to false.");
}
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected
@@ -311,15 +404,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
}
if(mVerboseLogging)
{
mLog.info("Session error caught.", cause);
}
disconnect();
}
@Override
public void messageReceived(IoSession session, Object object) throws Exception
{
if(object instanceof String)
if(object instanceof String message)
{
String message = (String) object;
if(mVerboseLogging)
{
mLog.info("Message Received [" + message + "]");
}
if(message != null && !message.trim().isEmpty())
{

View File

@@ -34,7 +34,8 @@
API_KEY("api_key"),
SOURCE_LIST("source_list"),
FREQ_LIST("freq_list"),
PATCHES("patches");
PATCH_LIST("patch_list"),
TALKER_ALIAS("talker_alias");
private String mHeader;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,55 +19,45 @@
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.ConnectException;
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;
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.alias.TalkerAliasIdentifier;
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.ConnectException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
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;
@@ -249,6 +239,7 @@
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
String talkgroup = getTo(audioRecording);
String radioId = getFrom(audioRecording);
String talkerAlias = getTalkerAlias(audioRecording);
Long frequency = getFrequency(audioRecording);
String patches = getPatches(audioRecording);
String talkgroupLabel = getTalkgroupLabel(audioRecording);
@@ -282,8 +273,8 @@
.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.PATCH_LIST, patches)
.addPart(FormField.TALKER_ALIAS, talkerAlias)
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
HttpRequest fileRequest = HttpRequest.newBuilder()
@@ -426,22 +417,40 @@
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*
*/
private static String getTo(AudioRecording audioRecording)
private String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
if(identifier != null)
{
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();
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
}
}
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";
@@ -547,6 +556,28 @@
return "[]";
}
/**
* Creates a formatted string with talkerAlias identifier
*/
private static String getTalkerAlias(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof TalkerAliasIdentifier)
{
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
if(talkerID.isValid())
{
return talkerID.getValue();
}
}
}
return "";
}
/**
* Tests both the connection and configuration against the OpenMHz Call API service
* @param configuration containing API key and system id

View File

@@ -36,6 +36,7 @@ public enum FormField
SOURCES("sources"),
SYSTEM("system"),
SYSTEM_LABEL("systemLabel"),
TALKER_ALIAS("talkerAlias"),
TALKGROUP_ID("talkgroup"),
TALKGROUP_GROUP("talkgroupGroup"),
TALKGROUP_LABEL("talkgroupLabel"),

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -34,6 +34,7 @@ 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.alias.TalkerAliasIdentifier;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
@@ -48,6 +49,7 @@ import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
@@ -227,6 +229,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
String talkgroup = getTo(audioRecording);
String radioId = getFrom(audioRecording);
String talkerAlias = getTalkerAlias(audioRecording);
Long frequency = getFrequency(audioRecording);
String patches = getPatches(audioRecording);
String talkgroupLabel = getTalkgroupLabel(audioRecording);
@@ -261,6 +264,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
.addPart(FormField.TALKGROUP_ID, talkgroup)
.addPart(FormField.SOURCE, radioId)
.addPart(FormField.FREQUENCY, frequency)
.addPart(FormField.TALKER_ALIAS, talkerAlias)
.addPart(FormField.TALKGROUP_LABEL, talkgroupLabel)
.addPart(FormField.TALKGROUP_GROUP, talkgroupGroup)
.addPart(FormField.SYSTEM_LABEL, systemLabel)
@@ -408,26 +412,62 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
return "0";
}
private static String getTalkerAlias(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof TalkerAliasIdentifier)
{
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
if(talkerID.isValid())
{
return talkerID.getValue();
}
}
}
return "";
}
/**
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*
*/
private static String getTo(AudioRecording audioRecording)
private String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
if(identifier != null)
{
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();
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
}
}
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";

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,17 @@ import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.util.ThreadPool;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
@@ -41,17 +52,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
@@ -212,10 +212,20 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
if(to != null)
{
sb.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
}
else
{
sb.append("TO:").append(to);
}
if(!aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -49,6 +49,15 @@ import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.math3.util.FastMath;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
@@ -59,15 +68,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class);
@@ -303,13 +303,23 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas
if(to != null)
{
sbTitle2.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
if(!aliases.isEmpty())
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
sbTitle2.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
}
else
{
sbTitle2.append("TO:").append(to);
if(!aliases.isEmpty())
{
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
}
else

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -41,6 +41,9 @@ import jmbe.iface.IAudioCodecLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base audio module for protocols that use the JMBE audio codec.
*/
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
ISquelchStateListener
{
@@ -59,12 +62,23 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
loadConverter();
}
@Override
protected void closeAudioSegment()
{
super.closeAudioSegment();
//Reset the audio codec to clear any leftover frame data from the previous call.
if(mAudioCodec != null)
{
mAudioCodec.reset();
}
}
@Override
public void dispose()
{
super.dispose();
MyEventBus.getGlobalEventBus().unregister(this);
mAudioCodec = null;
}
public IAudioCodec getAudioCodec()

View File

@@ -36,7 +36,7 @@ import java.util.List;
* radio identifiers.
*/
@JsonRootName("mbe_call")
@JsonPropertyOrder({"protocol", "call_type", "from", "to", "encrypted", "frames"})
@JsonPropertyOrder({"protocol", "version", "call_type", "from", "to", "encrypted", "system", "site", "frames"})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MBECallSequence
{
@@ -77,6 +77,15 @@ public class MBECallSequence
mProtocol = protocol;
}
/**
* Gets the version number for the mbe file structure.
* Should be incremented only on breaking structural changes, not minor additions.
*/
@JsonProperty("version")
public Integer getVersion()
{
return 2;
}
/**
* Indicates if this sequences contains any audio frames

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -140,8 +140,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
try
@@ -151,8 +152,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
"MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
//Run the queue processor task every 100 milliseconds or 10 times a second
@@ -276,11 +278,11 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
if(mOutput == null)
{
LOGGING_SUPPRESSOR.error("null output", 5, "Audio Output is null - ignoring audio playback request");
LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request");
return;
}
if(buffer != null)
if(buffer != null && buffer.array().length > 0)
{
int wrote = 0;
@@ -301,100 +303,22 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
checkStart();
//Something is causing the source data line to fail to accept audio byte data via the write() method
//and this seems to have started around JDK22, maybe. We can detect when this happens and close() and
//reopen the data line to clear the error.
if(!mOutput.isRunning() && wrote <= 0 && buffer.array().length > 0)
//Something is causing the source data line to fail to accept audio byte data via the write() method
//and this seems to have started around JDK22, maybe. When this happens, close and then re-open the
//data line to clear the error state. Note: in testing this error condition, the line is showing
//the buffer is empty and the capacity is fully available, so it should have accepted attempts to write
//data, but it failed, as indicated by wrote=0.
if(!mOutput.isRunning() && wrote <= 0)
{
mOutput.close();
try
{
mOutput = (SourceDataLine)mMixer.getLine(mLineInfo);
if(mOutput != null)
{
try
{
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
mGainControl = (FloatControl) gain;
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
}
try
{
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
mMuteControl = (BooleanControl) mute;
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
}
LOGGING_SUPPRESSOR.info("reopen audio output success", 5,
"Closed and reopened audio output - success - mOutput is not null");
}
else
{
LOGGING_SUPPRESSOR.info("reopen audio output fail", 5,
"Closed and reopened audio output - fail - mOutput is null");
return;
}
mOutput.open();
}
catch(LineUnavailableException lue)
catch(Exception e)
{
LOGGING_SUPPRESSOR.error("reopen fail for lua", 3, "Attempt to reopen " +
"audio source data line failed", lue);
return;
}
try
{
mOutput.open(mAudioFormat, mRequestedBufferSize);
//Start threshold: buffer is full with 10% or less of capacity remaining
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
//Stop threshold: buffer is empty with 90% or more capacity available
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
try
{
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
mGainControl = (FloatControl) gain;
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" + getChannelName() +
"] after reopening the source data line");
}
try
{
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
mMuteControl = (BooleanControl) mute;
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" + getChannelName() +
"] after reopening the source data line");
}
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
mCanProcessAudio = true;
}
catch(LineUnavailableException e)
{
LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 3,
"Failed to (re)open the source data line for audio output - mixer [" +
mMixer.getMixerInfo().getName() + "]");
return;
mLog.error("Error after closing and attempting to reopen audio output", e);
}
}
}

View File

@@ -41,6 +41,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -65,6 +66,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
private ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
private AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
private ReentrantLock mAudioOutputLock = new ReentrantLock();
/**
* Constructs an instance.
@@ -95,6 +98,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
mLog.warn("No audio output devices available");
}
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
}
/**
@@ -189,36 +195,53 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
else if(audioSegment.isLinked())
{
for(AudioOutput audioOutput: mAudioOutputs)
mAudioOutputLock.lock();
try
{
if(audioOutput.isLinkedTo(audioSegment))
for(AudioOutput audioOutput: mAudioOutputs)
{
it.remove();
audioOutput.play(audioSegment);
if(audioOutput.isLinkedTo(audioSegment))
{
it.remove();
audioOutput.play(audioSegment);
}
}
}
finally
{
mAudioOutputLock.unlock();
}
}
}
//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()));
mAudioSegments.sort(mAudioSegmentPrioritySorter);
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
mAudioOutputLock.lock();
try
{
if(audioOutput.isEmpty())
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
{
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
if(audioOutput.isEmpty())
{
return;
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
{
return;
}
}
}
}
finally
{
mAudioOutputLock.unlock();
}
}
//Remove any audio segments marked as complete that didn't get assigned to an output
@@ -288,37 +311,39 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
if(mProcessingTask != null)
mAudioOutputLock.lock();
try
{
mProcessingTask.cancel(true);
for(AudioOutput audioOutput: mAudioOutputs)
{
audioOutput.dispose();
}
mAudioOutputs.clear();
switch(entry.getMixerChannel())
{
case MONO:
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
mAudioOutputs.add(mono);
break;
case STEREO:
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
mAudioOutputs.add(left);
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
mAudioOutputs.add(right);
break;
default:
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
}
}
finally
{
mAudioOutputLock.unlock();
}
for(AudioOutput audioOutput: mAudioOutputs)
{
audioOutput.dispose();
}
mAudioOutputs.clear();
switch(entry.getMixerChannel())
{
case MONO:
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
mAudioOutputs.add(mono);
break;
case STEREO:
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
mAudioOutputs.add(left);
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
mAudioOutputs.add(right);
break;
default:
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
}
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
mMixerChannelConfiguration = entry;
}
@@ -389,4 +414,30 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
}
}
/**
* Audio segment comparator for sorting audio segments by: 1)Playback priority and 2)Segment start time
*/
public class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
{
@Override
public int compare(AudioSegment segment1, AudioSegment segment2)
{
if(segment1 == null || segment2 == null)
{
return -1;
}
//If priority is the same, sort by start time
if(segment1.monitorPriorityProperty().get() == segment2.monitorPriorityProperty().get())
{
return Long.compare(segment1.getStartTimestamp(), segment2.getStartTimestamp());
}
//Otherwise, sort by priority
else
{
return Integer.compare(segment1.monitorPriorityProperty().get(), segment2.monitorPriorityProperty().get());
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -243,6 +243,16 @@ public class BinaryMessage extends BitSet
return mSize;
}
/**
* Current size of the message indicating the quantity of bits that have been added to this message. This value
* is less than or equal to the size() value.
* @return current size in bits.
*/
public int currentSize()
{
return mPointer;
}
public void setSize(int size)
{
mSize = size;
@@ -281,6 +291,20 @@ public class BinaryMessage extends BitSet
}
}
/**
* Adds bit1 and bit2 to this bitset.
*
* Note: this method does not provide overfill protection.
*
* @param bit1 to add.
* @param bit2 to add.
*/
public void add(boolean bit1, boolean bit2)
{
set(mPointer++, bit1);
set(mPointer++, bit2);
}
public String toString()
{
StringBuilder sb = new StringBuilder();
@@ -401,8 +425,8 @@ public class BinaryMessage extends BitSet
boolean[] returnValue = null;
if(startIndex >= 0 &&
startIndex < endIndex &&
endIndex < mSize)
startIndex < endIndex &&
endIndex < mSize)
{
returnValue = new boolean[endIndex - startIndex + 1];
@@ -433,7 +457,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -662,7 +686,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -697,6 +721,28 @@ public class BinaryMessage extends BitSet
}
}
/**
* Sets the integer value to the field described by the argument.
* @param value to set
* @param intField describing the indices of the field.
*/
public void setInt(int value, IntField intField)
{
for(int x = 0; x < intField.width(); x++)
{
int mask = 1 << (intField.width() - x - 1);
if((value & mask) == mask)
{
set(intField.start() + x);
}
else
{
clear(intField.start() + x);
}
}
}
/**
* Returns the byte value represented by the bit array
*
@@ -710,7 +756,7 @@ public class BinaryMessage extends BitSet
if(bits.length != 8)
{
throw new IllegalArgumentException("Invalid - there must be 8"
+ "indexes to form a proper byte");
+ "indexes to form a proper byte");
}
int value = 0;
@@ -742,7 +788,7 @@ public class BinaryMessage extends BitSet
if(bits.length != 8)
{
throw new IllegalArgumentException("Invalid - there must be 8"
+ "indexes to form a proper byte");
+ "indexes to form a proper byte");
}
int value = 0;
@@ -870,7 +916,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -902,7 +948,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -990,7 +1036,7 @@ public class BinaryMessage extends BitSet
else
{
throw new IllegalArgumentException("BitSetBuffer.getHex() "
+ "maximum array length is 63 bits");
+ "maximum array length is 63 bits");
}
}
@@ -1011,7 +1057,7 @@ public class BinaryMessage extends BitSet
if(FastMath.abs(end - start) > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -1086,7 +1132,7 @@ public class BinaryMessage extends BitSet
if(FastMath.abs(end - start) > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -1241,7 +1287,7 @@ public class BinaryMessage extends BitSet
if(!message.matches("[01]*"))
{
throw new IllegalArgumentException(
"Message must contain only zeros and ones");
"Message must contain only zeros and ones");
}
BinaryMessage buffer = new BinaryMessage(message.length());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -69,6 +69,16 @@ public record IntField(int start, int end)
return new IntField(start, (start + 3));
}
/**
* Utility constructor method for a field with one four bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length6(int start)
{
return new IntField(start, (start + 5));
}
/**
* Utility constructor method for a field with one octet of length.
* @param start index (inclusive)

View File

@@ -1,35 +1,31 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Multi-Sync pattern matcher. Accepts multiple sync detector objects for
* comparision against the incoming bit stream.
@@ -39,6 +35,7 @@ import java.util.List;
* of the Long.rotate methods will wrap the MSB around to the LSB and
* corrupt the matching value.
*/
@Deprecated //remove after P25P2 decoder is updated
public class MultiSyncPatternMatcher
{
private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class);

View File

@@ -1,23 +1,27 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import org.apache.commons.lang3.Validate;
@Deprecated //Remove after P25P2 decoder is updated.
public class SoftSyncDetector implements ISyncProcessor
{
private ISyncDetectListener mListener;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,7 +29,7 @@ import java.util.Iterator;
*/
public class ByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private static final int FRAGMENT_SIZE = 8192;
private final static float[] LOOKUP_VALUES;
private float mAverageDc;

View File

@@ -1,57 +1,153 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This 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/>
******************************************************************************/
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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;
import java.util.Arrays;
/**
* Averaging buffer backed by a ring buffer implemented with a float array.
*/
public class FloatAveragingBuffer
{
private float[] mBuffer;
private float mAverage = 0.0f;
private int mBufferSize;
private int mBufferPointer;
public FloatAveragingBuffer( int size )
{
mBufferSize = size;
mBuffer = new float[ size ];
}
public float get( float newValue )
{
float oldValue = mBuffer[ mBufferPointer ];
private final float[] mBuffer;
private final int mBufferSize;
private float mAverage = 0.0f;
private int mBufferPointer;
private boolean mRapidFill = false;
private int mRapidFillCounter = 0;
private int mRapidFillIncrement;
if( Float.isInfinite( newValue ) || Float.isNaN( newValue ) )
{
mAverage = mAverage - ( oldValue / mBufferSize );
/**
* Constructs an instance that uses a rapid fill increment value where the initial values get counted multiple times
* to more quickly get to the average value instead of walking up to the average from the default average value of 0.
* @param size of buffer
* @param rapidFillIncrement indicating how many value additions per individual add to execute until the averaging
* buffer is filled. After the initial fill state is achieved, reverts to using a single value addition.
*/
public FloatAveragingBuffer(int size, int rapidFillIncrement)
{
this(size);
mRapidFillIncrement = rapidFillIncrement;
mRapidFill = true;
}
mBuffer[ mBufferPointer++ ] = 0.0f;
}
else
{
mAverage = mAverage - ( oldValue / mBufferSize ) + ( newValue / mBufferSize );
/**
* Constructs an instance that uses a ring buffer of the specified size.
* @param size of the averaging buffer.
*/
public FloatAveragingBuffer(int size)
{
mBufferSize = size;
mBuffer = new float[size];
}
mBuffer[ mBufferPointer++ ] = newValue;
}
/**
* Resets this buffer to an empty state
*/
public void reset()
{
Arrays.fill(mBuffer, 0);
mBufferPointer = 0;
mAverage = 0.0f;
if( mBufferPointer >= mBufferSize )
{
mBufferPointer = 0;
}
if(mRapidFillIncrement > 0)
{
mRapidFill = true;
mRapidFillCounter = 0;
}
}
return mAverage;
}
/**
* Current average value.
* @return average
*/
public float getAverage()
{
return mAverage;
}
/**
* Adds the value to the buffer and updates the average.
* @param value to add
*/
public void add(float value)
{
if(mRapidFill)
{
for(int x = 0; x < mRapidFillIncrement; x++)
{
float oldValue = mBuffer[mBufferPointer];
if(Float.isInfinite(value) || Float.isNaN(value))
{
mAverage = mAverage - (oldValue / mBufferSize);
mBuffer[mBufferPointer++] = 0.0f;
}
else
{
mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize);
mBuffer[mBufferPointer++] = value;
}
if(mBufferPointer >= mBufferSize)
{
mBufferPointer = 0;
}
}
mRapidFillCounter += mRapidFillIncrement;
if(mRapidFillCounter > mBufferSize)
{
mRapidFill = false;
}
}
else
{
float oldValue = mBuffer[mBufferPointer];
if(Float.isInfinite(value) || Float.isNaN(value))
{
mAverage = mAverage - (oldValue / mBufferSize);
mBuffer[mBufferPointer++] = 0.0f;
}
else
{
mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize);
mBuffer[mBufferPointer++] = value;
}
if(mBufferPointer >= mBufferSize)
{
mBufferPointer = 0;
}
}
}
/**
* Adds the value to the buffer and calculates a new average.
* @param value to add
* @return updated average
*/
public float get(float value)
{
add(value);
return mAverage;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,7 +30,7 @@ import java.util.Iterator;
public abstract class AirspyBufferIterator<T> implements Iterator<T>
{
protected static final float SCALE_SIGNED_12_BIT_TO_FLOAT = 1.0f / 2048.0f;
protected static final int FRAGMENT_SIZE = 2048;
protected static final int FRAGMENT_SIZE = 4096;
protected static final float[] COEFFICIENTS =
HilbertTransform.convertHalfBandToHilbert(HilbertTransform.HALF_BAND_FILTER_47_TAP);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@ package io.github.dsheirer.channel.metadata;
import com.jidesoft.swing.JideSplitPane;
import com.jidesoft.swing.JideTabbedPane;
import io.github.dsheirer.channel.details.ChannelDetailPanel;
import io.github.dsheirer.gui.power.ChannelPowerPanel;
import io.github.dsheirer.gui.channel.ChannelSpectrumPanel;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
@@ -42,7 +42,7 @@ public class NowPlayingPanel extends JPanel
private final ChannelDetailPanel mChannelDetailPanel;
private final DecodeEventPanel mDecodeEventPanel;
private final MessageActivityPanel mMessageActivityPanel;
private final ChannelPowerPanel mChannelPowerPanel;
private final ChannelSpectrumPanel mChannelSpectrumSquelchPanel;
private JideTabbedPane mTabbedPane;
private JideSplitPane mSplitPane;
private boolean mDetailTabsVisible;
@@ -58,7 +58,7 @@ public class NowPlayingPanel extends JPanel
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
mChannelSpectrumSquelchPanel = new ChannelSpectrumPanel(playlistManager, settingsManager);
mDetailTabsVisible = detailTabsVisible;
init();
@@ -96,12 +96,12 @@ public class NowPlayingPanel extends JPanel
mTabbedPane.addTab("Details", mChannelDetailPanel);
mTabbedPane.addTab("Events", mDecodeEventPanel);
mTabbedPane.addTab("Messages", mMessageActivityPanel);
mTabbedPane.addTab("Channel", mChannelPowerPanel);
mTabbedPane.addTab("Channel", mChannelSpectrumSquelchPanel);
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)));
mTabbedPane.addChangeListener(e -> mChannelSpectrumSquelchPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
.indexOfComponent(mChannelSpectrumSquelchPanel)));
}
return mTabbedPane;
@@ -135,6 +135,6 @@ public class NowPlayingPanel extends JPanel
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelSpectrumSquelchPanel);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,11 +35,10 @@ import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.heartbeat.Heartbeat;
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider,
IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider,
IdentifierUpdateProvider, IOverflowListener
@@ -53,7 +52,8 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
private Channel mChannel;
protected boolean mSourceOverflow = false;
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
protected boolean mTeardownSequenceStarted = false;
protected boolean mTeardownSequenceCompleted = false;
//TODO: remove the IOverflowListener code from this class
@@ -66,6 +66,22 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
mChannel = channel;
}
/**
* Indicates if the teardown sequence was started.
*/
public boolean isTeardownSequenceCompleted()
{
return mTeardownSequenceCompleted;
}
/**
* Indicates if the teardown sequence was completed, meaning that the request disable channel event was dispatched.
*/
public boolean isTeardownSequenceStarted()
{
return mTeardownSequenceStarted;
}
/**
* Updates/replaces the current channel configuration with the argument.
*/
@@ -89,6 +105,11 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
*/
protected abstract void checkState();
/**
* Indicates if any timeslot is currently in a TEARDOWN state.
*/
public abstract boolean isTeardownState();
public abstract List<ChannelMetadata> getChannelMetadata();
public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -44,13 +44,12 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
@@ -166,6 +165,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
mStateMachineMap.get(timeslot).setState(State.IDLE);
break;
case TEARDOWN:
mTeardownSequenceStarted = true;
if(getChannel().isTrafficChannel())
{
checkTeardown();
@@ -206,7 +206,16 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
{
if(getChannel().isTrafficChannel())
{
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
try
{
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
}
catch(Throwable t)
{
mLog.error("Error broadcasting shutdown channel event", t);
}
mTeardownSequenceStarted = true;
}
else
{
@@ -238,6 +247,20 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
}
}
@Override
public boolean isTeardownState()
{
for(StateMachine stateMachine: mStateMachineMap.values())
{
if(stateMachine.getState() == State.TEARDOWN)
{
return true;
}
}
return false;
}
/**
* Creates configuration identifiers for the channel name, system, site and alias list name.
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -46,11 +46,10 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Channel state tracks the overall state of all processing modules and decoders configured for the channel and
@@ -163,9 +162,11 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
mStateMachine.setState(State.IDLE);
break;
case TEARDOWN:
mTeardownSequenceStarted = true;
if(getChannel().isTrafficChannel())
{
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
mTeardownSequenceCompleted = true;
}
else
{
@@ -175,6 +176,12 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
}
}
@Override
public boolean isTeardownState()
{
return mStateMachine.getState() == State.TEARDOWN;
}
@Override
protected void checkState()
{

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.controller.channel;
@@ -26,15 +23,13 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Channel Model
*/
@@ -55,6 +50,58 @@ public class ChannelModel implements Listener<ChannelEvent>
mTrafficChannels.addListener(changeListener);
}
/**
* Get a deduplicated list of alias list names from across the channel configurations.
* @return list of alias list names.
*/
public List<String> getAliasListNames()
{
List<String> aliasListNames = new ArrayList<>();
for(Channel channel : mChannels)
{
String aliasListName = channel.getAliasListName();
if(aliasListName != null && !aliasListName.isEmpty() && !aliasListNames.contains(aliasListName))
{
aliasListNames.add(aliasListName);
}
}
return aliasListNames;
}
/**
* Renames the alias list across the set of aliases.
* @param oldName currently used by the alias
* @param newName to apply to the alias
*/
public void renameAliasList(String oldName, String newName)
{
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
{
return;
}
mChannels.stream().filter(channel -> channel.getAliasListName().equals(oldName))
.forEach(channel -> channel.setAliasListName(newName));
}
/**
* Deletes any aliases that have the alias list name
* @param aliasListName to delete
*/
public void deleteAliasList(String aliasListName)
{
if(aliasListName == null || aliasListName.isEmpty())
{
return;
}
mChannels.stream().filter(channel -> channel.getAliasListName().equals(aliasListName))
.forEach(channel -> channel.setAliasListName(null));
}
/**
* Observable list of channel configurations managed by this model
*/
@@ -109,6 +156,45 @@ public class ChannelModel implements Listener<ChannelEvent>
return null;
}
/**
* Renames the alias list across all channels.
* @param existing alias list name
* @param renamed alias list name.
*/
public void renameAliaslistForChannels(String existing, String renamed)
{
if(existing == null || existing.isEmpty() || renamed == null || renamed.isEmpty())
{
return;
}
mChannels.forEach(channel -> {
if(channel.getAliasListName() != null && channel.getAliasListName().equals(existing))
{
channel.setAliasListName(renamed);
}
});
}
/**
* Removes the alias list name from any channels.
* @param name to delete
*/
public void deleteAliasListName(String name)
{
if(name == null || name.isEmpty())
{
return;
}
mChannels.forEach(channel -> {
if(channel.getAliasListName() != null && channel.getAliasListName().equals(name))
{
channel.setAliasListName(null);
}
});
}
/**
* Returns a list of unique system values from across the channel set
*/

View File

@@ -279,15 +279,27 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
break;
case REQUEST_DISABLE:
case NOTIFICATION_DELETE:
if(channel != null && channel.isProcessing())
if(channel != null)
{
try
{
stopProcessing(channel);
}
catch(ChannelException ce)
catch(Throwable t)
{
mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage());
mLog.error("Error stopping channel [" + channel + "]", t);
}
}
else
{
try
{
throw new IllegalArgumentException("Request to disable channel must have non-null channel");
}
catch(IllegalArgumentException iae)
{
mLog.error("Caught a [" + event.getEvent() + "] non-standard channel event - logging stack trace. " +
"This should not happen, please send this error to the developer.", iae);
}
}
break;
@@ -521,7 +533,14 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
if(addProcessingChain(channel, processingChain))
{
processingChain.start();
try
{
processingChain.start();
}
catch(Throwable t)
{
mLog.error("Error caught during processing chain startup - continuing", t);
}
if(GraphicsEnvironment.isHeadless())
{
@@ -627,8 +646,28 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
else
{
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(false));
//When we're in non-headless mode we have to change the processing property on the JavaFX
//event thread. However, if it hasn't yet been initialized (ie an FX window opened), we'll
//get an ISE. In that case, just set the property to false because there won't be any
//property listeners being triggered.
try
{
Platform.runLater(() -> {
try
{
channel.setProcessing(false);
}
catch(Exception e)
{
mLog.error("Error during channel stop while setting processing to false [" + channel +
"] - continuing channel stop process", e);
}
});
}
catch(IllegalStateException e)
{
channel.setProcessing(false);
}
}
try
@@ -847,6 +886,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
mLog.error("Error stopping channel [" + (toShutdown != null ? toShutdown.getName() : "unknown") +
"] with source error - " + ce.getMessage());
}
catch(Throwable t)
{
mLog.error("Error stopping channel [" + toShutdown + "]", t);
}
}
}
}
@@ -875,6 +918,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
sb.append("\tProcessing Chain - Processing: ").append(chain.isProcessing()).append("\n");
AbstractChannelState state = chain.getChannelState();
sb.append("Channel State: ").append(state.getClass()).append("\n");
sb.append(" Teardown Started:").append(state.isTeardownSequenceStarted()).append("\n");
sb.append(" Teardown Completed:").append(state.isTeardownSequenceCompleted()).append("\n");
for(ChannelMetadata metadata: state.getChannelMetadata())
{
sb.append(metadata.getDescription()).append("\n");

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -56,15 +56,14 @@ import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import java.text.DecimalFormat;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Set;
import java.util.TreeSet;
/**
* Utility methods for designing various types of filters.
*/
@@ -627,11 +626,13 @@ public class FilterFactory
* @param samplesPerSymbol - number of samples per symbol
* @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size
* @param alpha - roll-off factor.
* @param gain of the filter
* @return - filter coefficients
*/
public static float[] getRootRaisedCosine(double samplesPerSymbol, int symbolCount, float alpha)
{
int taps = (int)(samplesPerSymbol * symbolCount);
samplesPerSymbol /= 2;
int taps = (int)Math.round(samplesPerSymbol * symbolCount);
float scale = 0;
@@ -639,47 +640,47 @@ public class FilterFactory
for(int x = 0; x < taps; x++)
{
float index = (float)x - ((float)taps / 2.0f);
double index = x - (taps / 2.0f);
float x1 = (float)FastMath.PI * index / (float)samplesPerSymbol;
float x2 = 4.0f * alpha * index / (float)samplesPerSymbol;
float x3 = x2 * x2 - 1.0f;
double x1 = FastMath.PI * index / samplesPerSymbol;
double x2 = 4.0 * alpha * index / samplesPerSymbol;
double x3 = x2 * x2 - 1.0;
float numerator, denominator;
double numerator, denominator;
if(FastMath.abs(x3) >= 0.000001)
{
if(x != taps / 2)
{
numerator = (float)FastMath.cos((1.0 + alpha) * x1) +
(float)FastMath.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol);
numerator = FastMath.cos((1.0 + alpha) * x1) +
FastMath.sin((1.0 - alpha) * x1) / (4.0 * alpha * index / samplesPerSymbol);
}
else
{
numerator = (float)FastMath.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)FastMath.PI / (4.0f * alpha);
numerator = FastMath.cos((1.0 + alpha) * x1) + (1.0 - alpha) * FastMath.PI / (4.0 * alpha);
}
denominator = x3 * (float)FastMath.PI;
denominator = x3 * FastMath.PI;
}
else
{
if(alpha == 1.0f)
if(alpha == 1.0)
{
coefficients[x] = -1.0f;
continue;
}
x3 = (1.0f - alpha) * x1;
x2 = (1.0f + alpha) * x1;
x3 = (1.0 - alpha) * x1;
x2 = (1.0 + alpha) * x1;
numerator = (float)(FastMath.sin(x2) * (1.0f + alpha) * FastMath.PI - FastMath.cos(x3) * ((1.0 - alpha) * FastMath.PI *
(double)samplesPerSymbol) / (4.0 * alpha * index) + FastMath.sin(x3) * (double)samplesPerSymbol *
(double)samplesPerSymbol / (4.0 * alpha * index * index));
numerator = (FastMath.sin(x2) * (1.0 + alpha) * FastMath.PI - FastMath.cos(x3) * ((1.0 - alpha) * FastMath.PI *
samplesPerSymbol) / (4.0 * alpha * index) + FastMath.sin(x3) * samplesPerSymbol *
samplesPerSymbol / (4.0 * alpha * index * index));
denominator = (float)(-32.0 * FastMath.PI * alpha * alpha * index / (double)samplesPerSymbol);
denominator = -32.0 * FastMath.PI * alpha * alpha * index / samplesPerSymbol;
}
coefficients[x] = 4.0f * alpha * numerator / denominator;
coefficients[x] = (float)(4.0 * alpha * numerator / denominator);
scale += coefficients[x];
}
@@ -1210,13 +1211,20 @@ public class FilterFactory
{
DecimalFormat df = new DecimalFormat("0.000000");
int length = 15;
int length = 49;
float [] taps = FilterFactory.getHalfBand(length, WindowType.HAMMING);
for(int x = 0; x < length; x++)
try
{
mLog.debug("Tap: " + x + " Value: " + df.format(taps[x]));
float [] taps = FilterFactory.getSinc(0.5, length, WindowType.NONE);
for(int x = 0; x < length; x++)
{
mLog.debug("Tap: " + x + " Value: " + df.format(taps[x]));
}
}
catch(Exception e)
{
e.printStackTrace();
}
mLog.debug("Done!");

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -252,11 +252,16 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
synchronized(mBufferDispatcher)
{
mChannelSources.remove(channelSource);
mPolyphaseChannelizer.removeChannel(channelSource);
if(mPolyphaseChannelizer != null)
{
mPolyphaseChannelizer.removeChannel(channelSource);
}
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
//If this is the last/only channel, deregister to stop the sample buffers
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
{
mNativeBufferProvider.removeBufferListener(mBufferDispatcher);
mBufferDispatcher.stop();
@@ -297,10 +302,17 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
break;
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
case NOTIFICATION_RECORDING_FILE_LOADED:
//no-op
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//Re-broadcast this event to each channel so that the decoders can reset error tracking function(s).
List<TunerChannelSource> channels = new ArrayList<>(mChannelSources);
for(TunerChannelSource channel: channels)
{
channel.broadcastConsumerSourceEvent(sourceEvent);
}
break;
default:
mLog.info("Unrecognized source event: " + sourceEvent);
break;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,6 +42,7 @@ public class DecimationFilterFactory
switch(decimationRate)
{
case 0:
case 1:
return new RealDecimateX0Filter();
case 2:
return new RealDecimateX2Filter();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,7 +20,7 @@
package io.github.dsheirer.dsp.filter.design;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.window.WindowType;
import javafx.application.Application;
import javafx.scene.Scene;
@@ -59,12 +59,32 @@ public class FilterViewer extends Application
*/
private float[] getFilter()
{
int filterLength = 47;
WindowType windowType = WindowType.HAMMING;
int filterLength = 31;
WindowType windowType = WindowType.BLACKMAN_HARRIS_7;
float[] taps = FilterFactory.getHalfBand(filterLength, windowType);
FIRFilterSpecification specification = FIRFilterSpecification
.lowPassBuilder()
.sampleRate(12500)
.passBandCutoff(5200)
.passBandAmplitude(1.0).passBandRipple(0.01) //.01
.stopBandAmplitude(0.0).stopBandStart(6500) //6500
.stopBandRipple(0.01).build();
taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
float[] taps = null;
try
{
taps = FilterFactory.getTaps(specification);
}
catch(Exception fde) //FilterDesignException
{
System.out.println("Error");
}
// float[] taps = FilterFactory.getHighPass(8000, 3000, filterLength, windowType);
// float[] taps = FilterFactory.getRootRaisedCosine(50000.0 / 4800.0, 20, 0.2f);
// taps = HilbertTransform.HALF_BAND_FILTER_47_TAP;
if(taps == null)
{

View File

@@ -1,25 +1,24 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.interpolator;
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* Copyright (C) 2002,2012 Free Software Foundation, Inc.
* Ported to java from gnuradio - interpolator_taps.h
*
* This 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/>
******************************************************************************/
public abstract class Interpolator
{
public static final int NTAPS = 8;
@@ -158,4 +157,19 @@ public abstract class Interpolator
{ -1.98993e-04f, 1.24642e-03f, -5.41054e-03f, 9.98534e-01f, 7.89295e-03f, -2.76968e-03f, 8.53777e-04f, -1.54700e-04f }, // 127/128
{ 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 1.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f, 0.00000e+00f }, // 128/128
};
/**
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
* offset+4. The mu argument is translated into an index position between 0 and 128, where a mu of 0.0 will be
* converted to index zero and will be equal to the sample at index offset+3 and a mu of 1.0 will be equal to
* the sample at offset+4. All mu values between 0.0 and 1.0 will be converted to a 1 - 127 index and will
* produce an approximated value from among 127 interpolated sample points between indexes offset+3 and offset+4.
*
* @param samples - sample array of length at least offset + 7
* @param offset into the sample array for the start of the 8-value sequence
* @param mu - interpolated sample position between 0.0 and 1.0
* @return - interpolated sample value
*/
public abstract float filter(float[] samples, int offset, float mu);
}

View File

@@ -0,0 +1,57 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.interpolator;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
/**
* Factory for creating an optimal scalar or vector (SIMD) interpolator implementation
*/
public class InterpolatorFactory
{
/**
* Selects and instantiates the best interpolator version, scalar or vector, based on previous calibration.
* @return interpolator
*/
public static Interpolator getInterpolator()
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.INTERPOLATOR);
return getInterpolator(implementation);
}
/**
* Instantiates the specified interpolator implementation.
* @param implementation to construct.
* @return interpolator.
*/
public static Interpolator getInterpolator(Implementation implementation)
{
return switch(implementation)
{
case VECTOR_SIMD_64 -> new InterpolatorVector64();
case VECTOR_SIMD_128 -> new InterpolatorVector128();
case VECTOR_SIMD_256, VECTOR_SIMD_512 -> new InterpolatorVector256(); //Vector 512 not supported
case SCALAR, UNCALIBRATED -> new InterpolatorScalar();
default -> throw new IllegalArgumentException("Unknown implementation type: " + implementation);
};
}
}

View File

@@ -1,17 +1,35 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.interpolator;
import io.github.dsheirer.sample.complex.Complex;
import java.text.DecimalFormat;
import java.util.Arrays;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.Arrays;
public class RealInterpolator extends Interpolator
public class InterpolatorScalar extends Interpolator
{
private final static Logger mLog = LoggerFactory.getLogger(RealInterpolator.class);
private final static Logger mLog = LoggerFactory.getLogger(InterpolatorScalar.class);
private float mGain;
@@ -21,11 +39,16 @@ public class RealInterpolator extends Interpolator
*
* @param gain to apply to the interpolated sample
*/
public RealInterpolator(float gain)
public InterpolatorScalar(float gain)
{
mGain = gain;
}
public InterpolatorScalar()
{
this(1.0f);
}
/**
* Calculates an interpolated value from eight samples that start at the offset into the sample array. The
* interpolated sample will fall within the middle of the eight sample array, between indexes offset+3 and
@@ -68,7 +91,7 @@ public class RealInterpolator extends Interpolator
public static void main(String[] args)
{
RealInterpolator interpolator = new RealInterpolator(1.0f);
InterpolatorScalar interpolator = new InterpolatorScalar(1.0f);
DecimalFormat decimalFormat = new DecimalFormat("0.0000");
double TWO_PI = FastMath.PI * 2.0;

View File

@@ -0,0 +1,49 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.interpolator;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector interpolator for 128 bit SIMD instructions.
*/
public class InterpolatorVector128 extends VectorInterpolator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
@Override
protected float vectorFilter(float[] samples, int offset, int index)
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
FloatVector tapsVector, sampleVector;
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
accumulator = tapsVector.fma(sampleVector, accumulator);
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 4);
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 4);
accumulator = tapsVector.fma(sampleVector, accumulator);
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,40 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.interpolator;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector interpolator for 256 bit SIMD instructions.
*/
public class InterpolatorVector256 extends VectorInterpolator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
@Override
protected float vectorFilter(float[] samples, int offset, int index)
{
FloatVector tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
FloatVector sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
return sampleVector.mul(tapsVector).reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,57 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.interpolator;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector interpolator for 64 bit SIMD instructions.
*/
public class InterpolatorVector64 extends VectorInterpolator
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
@Override
protected float vectorFilter(float[] samples, int offset, int index)
{
FloatVector accumulator = FloatVector.zero(VECTOR_SPECIES);
FloatVector tapsVector, sampleVector;
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 0);
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset);
accumulator = tapsVector.fma(sampleVector, accumulator);
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 2);
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 2);
accumulator = tapsVector.fma(sampleVector, accumulator);
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 4);
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 4);
accumulator = tapsVector.fma(sampleVector, accumulator);
tapsVector = FloatVector.fromArray(VECTOR_SPECIES, TAPS[index], 6);
sampleVector = FloatVector.fromArray(VECTOR_SPECIES, samples, offset + 6);
accumulator = tapsVector.fma(sampleVector, accumulator);
return accumulator.reduceLanes(VectorOperators.ADD);
}
}

View File

@@ -0,0 +1,52 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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.interpolator;
/**
* Linear interpolator.
*/
public class LinearInterpolator
{
/**
* Calculates an interpolated value between x1 and x2 at a linear position mu between 0.0 and 1.0
* @param x1 first value
* @param x2 second value
* @param mu offset between first and second values in range 0.0 to 1.0
* @return interpolated value.
*/
public static float calculate(float x1, float x2, float mu)
{
if(mu < 0)
{
return x1;
}
else if(mu > 1)
{
return x2;
}
return x1 + ((x2 - x1) * mu);
}
public static float calculate(float x1, float x2, double mu)
{
return calculate(x1, x2, (float)mu);
}
}

View File

@@ -0,0 +1,50 @@
/*
* *****************************************************************************
* 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.interpolator;
import org.apache.commons.lang3.Validate;
/**
* Base vector interpolator implementation.
*/
public abstract class VectorInterpolator extends Interpolator
{
@Override
public float filter(float[] samples, int offset, float mu)
{
/* Ensure we have enough samples in the array */
Validate.isTrue(samples.length >= offset + 7, "Offset [" + offset + "] must be 7 less than length[" + samples.length + "]");
//Identify the filter bank that corresponds to mu. Note: since we're not loading the TAPS in reverse order,
//we select the inverse tap index that has the mirrored set of taps, by subtracting from 127.
int index = 127 - (int)(NSTEPS * mu);
return vectorFilter(samples, offset, index);
}
/**
* Perform vectorized interpolation to find the interpolated value from the samples where the 8-value sequence
* starts at the offset into the array, using the filter coefficients specified by the index value.
* @param samples to interpolate
* @param offset to the start of the 8-value sequence
* @param index into the TAPS coefficients array.
* @return interpolated value.
*/
protected abstract float vectorFilter(float[] samples, int offset, int index);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,10 +19,6 @@
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;
@@ -32,63 +28,22 @@ import org.slf4j.LoggerFactory;
public abstract class VectorFMDemodulator implements IDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator.class);
protected static final float ZERO = 0.0f;
private static final int BUFFER_OVERLAP = 1;
//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];
private ScalarFMDemodulator mScalar;
public VectorFMDemodulator()
{
}
/**
* Vector species as specified by the implementing sub-class.
* Access the scalar implementation when the sample buffer size is not a multiple of the vector size.
*/
protected abstract VectorSpecies<Float> getVectorSpecies();
@Override public float[] demodulate(float[] i, float[] q)
protected ScalarFMDemodulator getScalarImplementation()
{
// VectorSpecies<Float> species= getVectorSpecies();
VectorUtilities.checkComplexArrayLength(i, q, getVectorSpecies());
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
if(mScalar == null)
{
mIBuffer = new float[i.length + BUFFER_OVERLAP];
mQBuffer = new float[q.length + BUFFER_OVERLAP];
mScalar = new ScalarFMDemodulator();
}
//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 += getVectorSpecies().length())
{
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()));
//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;
return mScalar;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,6 @@
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;
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator128 implements IDemodulator
public class VectorFMDemodulator128 extends VectorFMDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator128.class);
protected static final float ZERO = 0.0f;
@@ -44,9 +43,14 @@ public class VectorFMDemodulator128 implements IDemodulator
{
}
@Override public float[] demodulate(float[] i, float[] q)
@Override
public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
if(i.length % VECTOR_SPECIES.length() != 0)
{
return getScalarImplementation().demodulate(i, q);
}
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,6 @@
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;
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator256 implements IDemodulator
public class VectorFMDemodulator256 extends VectorFMDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator256.class);
protected static final float ZERO = 0.0f;
@@ -44,9 +43,14 @@ public class VectorFMDemodulator256 implements IDemodulator
{
}
@Override public float[] demodulate(float[] i, float[] q)
@Override
public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
if(i.length % VECTOR_SPECIES.length() != 0)
{
return getScalarImplementation().demodulate(i,q);
}
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,6 @@
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;
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator512 implements IDemodulator
public class VectorFMDemodulator512 extends VectorFMDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator512.class);
protected static final float ZERO = 0.0f;
@@ -44,9 +43,14 @@ public class VectorFMDemodulator512 implements IDemodulator
{
}
@Override public float[] demodulate(float[] i, float[] q)
@Override
public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
if(i.length % VECTOR_SPECIES.length() != 0)
{
return getScalarImplementation().demodulate(i,q);
}
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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,6 @@
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;
@@ -29,7 +28,7 @@ import org.slf4j.LoggerFactory;
/**
* FM demodulator that uses JDK 17+ SIMD vector intrinsics
*/
public class VectorFMDemodulator64 implements IDemodulator
public class VectorFMDemodulator64 extends VectorFMDemodulator
{
private static final Logger mLog = LoggerFactory.getLogger(VectorFMDemodulator64.class);
protected static final float ZERO = 0.0f;
@@ -44,9 +43,14 @@ public class VectorFMDemodulator64 implements IDemodulator
{
}
@Override public float[] demodulate(float[] i, float[] q)
@Override
public float[] demodulate(float[] i, float[] q)
{
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
//Fallback to scalar implementation if the buffer size is not a multiple of the vector implementation.
if(i.length % VECTOR_SPECIES.length() != 0)
{
return getScalarImplementation().demodulate(i,q);
}
if(mIBuffer.length != (i.length + BUFFER_OVERLAP))
{

View File

@@ -1,90 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public class DQPSKDecisionDirectedDemodulator extends PSKDemodulator<Dibit>
{
protected DQPSKDecisionDirectedSymbolEvaluator mSymbolEvaluator = new DQPSKDecisionDirectedSymbolEvaluator();
private Complex mPreviousPrecedingSample = new Complex(0, 0);
private Complex mPreviousCurrentSample = new Complex(0, 0);
private Complex mPrecedingSample = new Complex(0, 0);
private Complex mCurrentSample = new Complex(0, 0);
private Complex mPrecedingSymbol = new Complex(0, 0);
protected Complex mCurrentSymbol = new Complex(0, 0);
/**
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
* a decision-directed DQPSK symbol timing and phase error detector to automatically align to the incoming carrier
* frequency and to adjust for any changes in symbol timing.
*
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
*/
public DQPSKDecisionDirectedDemodulator(IPhaseLockedLoop phaseLockedLoop, InterpolatingSampleBuffer interpolatingSampleBuffer)
{
super(interpolatingSampleBuffer, phaseLockedLoop);
}
/**
* Calculates a symbol from the interpolating buffer
*/
protected void calculateSymbol()
{
//Get preceding sample and an interpolated current sample from the interpolating buffer
mPrecedingSample = getInterpolatingSampleBuffer().getPrecedingSample();
mCurrentSample = getInterpolatingSampleBuffer().getCurrentSample();
//Differential decode preceding and current symbols by calculating the angular rotation between the previous and
//current samples (current sample x complex conjugate of previous sample).
//Note: preceding symbol is a preceding measurement of the current symbol that is simply used as a reference
//point to determine vector rotation to the current symbol -- it is not the true predecessor symbol
mPrecedingSymbol.setInphase(Complex.multiplyInphase(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
mPrecedingSymbol.setQuadrature(Complex.multiplyQuadrature(mPrecedingSample.inphase(), mPrecedingSample.quadrature(),
mPreviousPrecedingSample.inphase(), -mPreviousPrecedingSample.quadrature()));
mCurrentSymbol.setInphase(Complex.multiplyInphase(mCurrentSample.inphase(), mCurrentSample.quadrature(),
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
mCurrentSymbol.setQuadrature(Complex.multiplyQuadrature(mCurrentSample.inphase(), mCurrentSample.quadrature(),
mPreviousCurrentSample.inphase(), -mPreviousCurrentSample.quadrature()));
//Set gain to unity before we calculate the error value
mPrecedingSymbol.normalize();
mCurrentSymbol.normalize();
//Apply symbols to evaluator to calculate phase and timing error and make a symbol decision
mSymbolEvaluator.setSymbol(mPrecedingSymbol, mCurrentSymbol);
//Update the symbol timing error
getInterpolatingSampleBuffer().resetAndAdjust(mSymbolEvaluator.getTimingError());
//Update the costas loop (PLL) with any measured phase error
getPLL().adjust(clip(mSymbolEvaluator.getPhaseError(), 0.5f));
//Store current samples/symbols to use for the next symbol period
mPreviousPrecedingSample.setValues(mPrecedingSample);
mPreviousCurrentSample.setValues(mCurrentSample);
broadcast(mSymbolEvaluator.getSymbolDecision());
}
}

View File

@@ -1,147 +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.psk;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
import org.apache.commons.math3.util.FastMath;
public class DQPSKDecisionDirectedDemodulatorInstrumented extends DQPSKDecisionDirectedDemodulator
{
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mComplexSymbolListener;
private Listener<Double> mPLLErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<ComplexSamples> mFilteredGainAppliedComplexBufferListener;
private double mSampleRate;
/**
* Decoder for Differential Quaternary Phase Shift Keying (DQPSK). This decoder uses both a Costas Loop (PLL) and
* a custom DQPSK symbol timing error detector to automatically align to the incoming carrier frequency and to
* adjust for any changes in symbol timing.
*
* This detector is optimized for constant amplitude DQPSK symbols like C4FM.
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param interpolatingSampleBuffer
* @param sampleRate of the incoming complex sample stream
*/
public DQPSKDecisionDirectedDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
InterpolatingSampleBufferInstrumented interpolatingSampleBuffer,
double sampleRate)
{
super(phaseLockedLoop, interpolatingSampleBuffer);
mSampleRate = sampleRate;
}
@Override
public void receive(ComplexSamples complexSamples)
{
if(mFilteredGainAppliedComplexBufferListener != null)
{
mFilteredGainAppliedComplexBufferListener.receive(complexSamples);
}
super.receive(complexSamples);
}
/**
* Overrides the parent class symbol calculation to capture eye diagram data
*/
protected void calculateSymbol()
{
//Eye diagram listener
if(mSymbolDecisionDataListener != null)
{
mSymbolDecisionDataListener.receive(
((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData());
}
super.calculateSymbol();
if(mSamplesPerSymbolListener != null)
{
mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
}
//Send to an external constellation symbol listener when registered
if(mComplexSymbolListener != null)
{
mComplexSymbolListener.receive(mCurrentSymbol);
}
if(mPLLErrorListener != null)
{
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
}
if(mPLLFrequencyListener != null)
{
double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency();
loopFrequency *= mSampleRate / (2.0 * FastMath.PI);
mPLLFrequencyListener.receive(loopFrequency);
}
}
/**
* Registers a listener to receive symbol decision data to produce an eye diagram.
*/
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
}
/**
* Registers the listener to receive decoded QPSK symbols
*/
public void setComplexSymbolListener(Listener<Complex> listener)
{
mComplexSymbolListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLErrorListener(Listener<Double> listener)
{
mPLLErrorListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
}
}

View File

@@ -1,172 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
public class DQPSKDecisionDirectedSymbolEvaluator implements IPSKSymbolEvaluator<Dibit>
{
private static final Complex ROTATE_FROM_PLUS_135 = Complex.fromAngle(-3.0 * FastMath.PI / 4.0);
private static final Complex ROTATE_FROM_PLUS_45 = Complex.fromAngle(-1.0 * FastMath.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_45 = Complex.fromAngle(1.0 * FastMath.PI / 4.0);
private static final Complex ROTATE_FROM_MINUS_135 = Complex.fromAngle(3.0 * FastMath.PI / 4.0);
private Complex mEvaluationSymbol = new Complex(0, 0);
private float mPhaseError = 0.0f;
private float mTimingError = 0.0f;
private float mTimingErrorPolarity = 1.0f;
private Dibit mSymbolDecision = Dibit.D00_PLUS_1;
/**
* Differential QPSK Decision-directed symbol phase and timing error detector and symbol decision slicer.
*
* Symbol decision is based on the closest reference quadrant for the sampled symbol.
*
* Phase and timing error are calculated as the angular distance of the sampled symbol from the reference
* symbol. Timing error is further corrected to compensate for clock-wise or counter-clock-wise vector rotation
* relative to the immediately preceding symbol.
*/
public DQPSKDecisionDirectedSymbolEvaluator()
{
}
/**
* Sets the preceding and current symbols to be evaluated for phase and timing errors and to determine the
* transmitted symbol relative to the closest reference symbol. After invoking this method, you can access the
* phase and timing errors and the symbol decision via their respective accessor methods.
*
* Phase and timing error values are calculated by first determining the symbol and then calculating the phase
* and timing errors relative to the reference symbol. The timing error is corrected with the appropriate sign
* relative to the angular vector rotation so that the error value indicates the correct error direction.
*
* @param preceding differential-decoded sample/symbol used to detect rotation - does NOT need to be interpolated
* since it's only a reference
* @param current symbol to be evaluated - interpolated
*/
public void setSymbol(Complex preceding, Complex current)
{
mEvaluationSymbol.setValues(current);
if(mEvaluationSymbol.quadrature() > 0.0f)
{
if(mEvaluationSymbol.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D00_PLUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_45);
}
else
{
mSymbolDecision = Dibit.D01_PLUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mEvaluationSymbol.multiply(ROTATE_FROM_PLUS_135);
}
}
else
{
if(mEvaluationSymbol.inphase() > 0.0f)
{
mSymbolDecision = Dibit.D10_MINUS_1;
mTimingErrorPolarity = (preceding.quadrature() > current.quadrature() ? 1.0f : -1.0f);
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_45);
}
else
{
mSymbolDecision = Dibit.D11_MINUS_3;
mTimingErrorPolarity = (preceding.quadrature() < current.quadrature() ? 1.0f : -1.0f);
mEvaluationSymbol.multiply(ROTATE_FROM_MINUS_135);
}
}
//Since we've rotated the error symbol back to 0 radians, the quadrature value closely approximates the
//arctan of the error angle relative to 0 radians and this provides our error value
float errorNormalized = normalize(mEvaluationSymbol.quadrature(), 0.3f);
mPhaseError = -errorNormalized;
//Timing error is the same as phase error with the sign corrected according to the vector's angular rotation
mTimingError = errorNormalized * mTimingErrorPolarity;
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
private static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
/**
* Constrains timing error to +/- the maximum value and corrects any
* floating point invalid numbers
*/
private float normalize(float error, float maximum)
{
if(Float.isNaN(error))
{
return 0.0f;
}
else
{
return clip(error, maximum);
}
}
/**
* Phase error of the symbol relative to the nearest reference symbol.
*
* @return phase error in radians of distance from the reference symbol.
*/
@Override
public float getPhaseError()
{
return mPhaseError;
}
/**
* Timing error of the symbol relative to the nearest reference symbol.
*
* @return timing error in radians of angular distance from the reference symbol recognizing that the symbol
* originates at zero radians and rotates toward the intended reference symbol, therefore the error value indicates
* if the symbol was sampled early (-) or late (+) relative to the reference symbol.
*/
@Override
public float getTimingError()
{
return mTimingError;
}
/**
* Reference symbol that is closest to the transmitted/sampled symbol.
*/
@Override
public Dibit getSymbolDecision()
{
return mSymbolDecision;
}
}

View File

@@ -1,152 +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.psk;
import io.github.dsheirer.dsp.psk.pll.CostasLoop;
import io.github.dsheirer.dsp.psk.pll.IPhaseLockedLoop;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSamples;
import org.apache.commons.math3.util.FastMath;
public class DQPSKGardnerDemodulatorInstrumented extends DQPSKGardnerDemodulator
{
private Listener<SymbolDecisionData> mSymbolDecisionDataListener;
private Listener<Double> mSamplesPerSymbolListener;
private Listener<Complex> mComplexSymbolListener;
private Listener<Double> mPLLErrorListener;
private Listener<Double> mPLLFrequencyListener;
private Listener<ComplexSamples> mFilteredGainAppliedComplexBufferListener;
private double mSampleRate;
/**
* Implements a Differential QPSK demodulator using a Costas Loop (PLL) and a Gardner timing error detector.
*
* This decoder is optimized for P25 Linear Simulcast Modulation (LSM).
*
* @param phaseLockedLoop for tracking carrier frequency error
* @param interpolatingSampleBuffer to hold samples for interpolating a symbol
*/
public DQPSKGardnerDemodulatorInstrumented(IPhaseLockedLoop phaseLockedLoop,
InterpolatingSampleBuffer interpolatingSampleBuffer,
double sampleRate)
{
super(phaseLockedLoop, interpolatingSampleBuffer);
mSampleRate = sampleRate;
}
@Override
public void receive(ComplexSamples complexSamples)
{
if(mFilteredGainAppliedComplexBufferListener != null)
{
mFilteredGainAppliedComplexBufferListener.receive(complexSamples);
}
super.receive(complexSamples);
}
/**
* Overrides the parent class symbol calculation to capture eye diagram data
*/
protected void calculateSymbol()
{
//Eye diagram listener
if(mSymbolDecisionDataListener != null)
{
mSymbolDecisionDataListener.receive(
((InterpolatingSampleBufferInstrumented)getInterpolatingSampleBuffer()).getSymbolDecisionData());
}
super.calculateSymbol();
if(mSamplesPerSymbolListener != null)
{
mSamplesPerSymbolListener.receive((double)getInterpolatingSampleBuffer().getSamplingPoint());
}
//Send to an external constellation symbol listener when registered
if(mComplexSymbolListener != null)
{
mComplexSymbolListener.receive(mCurrentSymbol);
}
if(mPLLErrorListener != null)
{
mPLLErrorListener.receive((double)mSymbolEvaluator.getPhaseError());
}
if(mPLLFrequencyListener != null)
{
double loopFrequency = ((CostasLoop)getPLL()).getLoopFrequency();
loopFrequency *= mSampleRate / (2.0 * FastMath.PI);
mPLLFrequencyListener.receive(loopFrequency);
}
}
/**
* Registers a listener to receive symbol decision data to produce an eye diagram.
*/
public void setSymbolDecisionDataListener(Listener<SymbolDecisionData> listener)
{
mSymbolDecisionDataListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setSamplesPerSymbolListener(Listener<Double> listener)
{
mSamplesPerSymbolListener = listener;
}
/**
* Registers the listener to receive decoded QPSK symbols
*/
public void setComplexSymbolListener(Listener<Complex> listener)
{
mComplexSymbolListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLErrorListener(Listener<Double> listener)
{
mPLLErrorListener = listener;
}
/**
* Registers the listener to receive PLL error values
*/
public void setPLLFrequencyListener(Listener<Double> listener)
{
mPLLFrequencyListener = listener;
}
/**
* Registers the listener to receive complex sample buffers that have been filtered with automatic gain control applied
*/
public void setFilteredGainAppliedComplexBufferListener(Listener<ComplexSamples> listener)
{
mFilteredGainAppliedComplexBufferListener = listener;
}
}

View File

@@ -1,83 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
public class GardnerDetector
{
private float mErrorInphase;
private float mErrorQuadrature;
private float mError;
/**
* Gardner detector for recovering symbol timing error.
*/
public GardnerDetector()
{
}
/**
* Calculates the symbol timing error by evaluating the previous, middle and current symbol instants.
*
* @param previous symbol
* @param middle symbol (ie midway between the previous and current)
* @param current symbol
* @return symbol timing error
*/
public float getError(Complex previous, Complex middle, Complex current)
{
mErrorInphase = (FastMath.abs(previous.inphase()) - FastMath.abs(current.inphase())) * FastMath.abs(middle.inphase());
mErrorQuadrature = (FastMath.abs(previous.quadrature()) - FastMath.abs(current.quadrature())) * FastMath.abs(middle.quadrature());
mError = normalize(mErrorInphase + mErrorQuadrature, 1.0f);
return mError;
}
/**
* Constrains timing error to +/- the maximum value and corrects any floating point invalid numbers
*/
public static float normalize(float error, float maximum)
{
if(Float.isNaN(error))
{
return 0.0f;
}
else
{
return clip(error, maximum);
}
}
/**
* Constrains value to the range of ( -maximum <> maximum )
*/
public static float clip(float value, float maximum)
{
if(value > maximum)
{
return maximum;
}
else if(value < -maximum)
{
return -maximum;
}
return value;
}
}

View File

@@ -1,29 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.complex.Complex;
public interface IQPSKSymbolDecoder
{
/**
* Decodes the symbol and converts it into a Dibit
* @param symbol to decode
* @return decoded dibit
*/
Dibit decode(Complex symbol);
}

View File

@@ -1,28 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
public interface ISymbolPhaseErrorCalculator
{
/**
* Calculates the phase error of the symbol relative to an alignment for the constellation
* @param symbol to calculate error
* @return error value
*/
float getPhaseError(Complex symbol);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,7 +18,7 @@
*/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.filter.interpolator.RealInterpolator;
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorScalar;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
@@ -45,7 +45,7 @@ public class InterpolatingSampleBuffer
private float mMaximumSamplesPerSymbol;
private float mMinimumSamplesPerSymbol;
private RealInterpolator mInterpolator = new RealInterpolator(1.0f);
private InterpolatorScalar mInterpolator = new InterpolatorScalar(1.0f);
/**
* Buffer to store complex sample data and produce interpolated samples.

View File

@@ -1,80 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InterpolatingSampleBufferInstrumented extends InterpolatingSampleBuffer
{
private final static Logger mLog = LoggerFactory.getLogger(InterpolatingSampleBufferInstrumented.class);
private SymbolDecisionData mSymbolDecisionData;
private ComplexSampleListener mSampleListener;
private int mBufferLength;
public InterpolatingSampleBufferInstrumented(float samplesPerSymbol, float symbolTimingGain)
{
super(samplesPerSymbol, symbolTimingGain);
mBufferLength = (int) FastMath.ceil(samplesPerSymbol);
mSymbolDecisionData = new SymbolDecisionData(mBufferLength);
}
public void receive(Complex sample)
{
super.receive(sample);
mSymbolDecisionData.receive(sample);
if(mSampleListener != null)
{
mSampleListener.receive(sample.inphase(), sample.quadrature());
}
}
/**
* Contents of the interpolating buffer and the current buffer index and symbol decision offset. This data can
* be used to support an external eye-diagram chart.
* @return symbol decision data.
*/
public SymbolDecisionData getSymbolDecisionData()
{
for(int x = mDelayLinePointer; x < mDelayLinePointer + mBufferLength; x++)
{
mSymbolDecisionData.receive(mDelayLineInphase[x], mDelayLineQuadrature[x]);
}
mSymbolDecisionData.setSamplingPoint(getSamplingPoint());
return mSymbolDecisionData;
}
/**
* Sets the listener to receive samples being sent to this buffer. Note: these samples have
* already been corrected by the PLL, so this provides an ideal tap point for PLL corrected samples.
* @param listener to receive samples.
*/
public void setSampleListener(ComplexSampleListener listener)
{
mSampleListener = listener;
}
}

View File

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

View File

@@ -1,73 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
public class QPSKStarSlicer implements IQPSKSymbolDecoder, Listener<Complex>
{
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
/**
* Slices a Complex sample representing a phase shifted symbol according to the following constellation pattern:
*
* 00 +3 | 01 +1
* ------|------
* 10 -1 | 11 -1
*/
public QPSKStarSlicer()
{
}
public void dispose()
{
mBroadcaster.dispose();
mBroadcaster = null;
}
public void addListener(Listener<Dibit> listener)
{
mBroadcaster.addListener(listener);
}
public void removeListener(Listener<Dibit> listener)
{
mBroadcaster.removeListener(listener);
}
@Override
public void receive(Complex sample)
{
mBroadcaster.broadcast(decode(sample));
}
@Override
public Dibit decode(Complex sample)
{
if(sample.inphase() > 0)
{
return sample.quadrature() > 0 ? Dibit.D00_PLUS_1 : Dibit.D10_MINUS_1;
}
else
{
return sample.quadrature() > 0 ? Dibit.D01_PLUS_3 : Dibit.D11_MINUS_3;
}
}
}

View File

@@ -1,91 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.dsp.symbol.Dibit;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
public class QPSKSymbolDecoder implements Listener<Complex>, IQPSKSymbolDecoder
{
private Broadcaster<Dibit> mBroadcaster = new Broadcaster<Dibit>();
/**
* Slices a ComplexSample representing a phase shifted symbol according to
* the following constellation pattern:
*
* \ 00 /
* \ /
* 01 \/ 10
* /\
* / \
* / 11 \
*/
public QPSKSymbolDecoder()
{
}
public void dispose()
{
mBroadcaster.dispose();
mBroadcaster = null;
}
public void addListener( Listener<Dibit> listener )
{
mBroadcaster.addListener( listener );
}
public void removeListener( Listener<Dibit> listener )
{
mBroadcaster.removeListener( listener );
}
@Override
public void receive( Complex complex )
{
mBroadcaster.broadcast( decode( complex ) );
}
public Dibit decode(Complex complex )
{
if( FastMath.abs( complex.inphase() ) > FastMath.abs( complex.quadrature() ) )
{
if( complex.inphase() > 0 )
{
return Dibit.D10_MINUS_1;
}
else
{
return Dibit.D01_PLUS_3;
}
}
else
{
if( complex.quadrature() > 0 )
{
return Dibit.D00_PLUS_1;
}
else
{
return Dibit.D11_MINUS_3;
}
}
}
}

View File

@@ -1,58 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
/**
* Calculates the phase error for a QPSK polar-orientation constellation
*/
public class QPSKSymbolPhaseErrorCalculator implements ISymbolPhaseErrorCalculator
{
private float mPhaseError;
@Override
public float getPhaseError(Complex symbol)
{
mPhaseError = 0.0f;
if(FastMath.abs(symbol.inphase()) > FastMath.abs(symbol.quadrature()))
{
if(symbol.inphase() > 0)
{
mPhaseError = -symbol.quadrature();
}
else
{
mPhaseError = symbol.quadrature();
}
}
else
{
if(symbol.quadrature() > 0)
{
mPhaseError = symbol.inphase();
}
else
{
mPhaseError = -symbol.inphase();
}
}
return mPhaseError;
}
}

View File

@@ -1,115 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.dsp.psk;
import io.github.dsheirer.buffer.ComplexCircularBuffer;
import io.github.dsheirer.sample.complex.Complex;
import org.apache.commons.math3.util.FastMath;
public class SymbolDecisionData
{
//45 degrees rotation to orient the symbol to a polar axis to make error calculation easy/efficient
public static final Complex DIFFERENTIAL_OFFSET = Complex.fromAngle(FastMath.PI / 4.0d);
private ComplexCircularBuffer mBuffer;
private float mSamplingPoint;
private float mSamplesPerSymbol;
/**
* Circular buffer for capturing two symbols worth of sample data for use in instrumentation.
* @param samplesPerSymbol to size the buffer
*/
public SymbolDecisionData(float samplesPerSymbol)
{
mSamplesPerSymbol = samplesPerSymbol;
mBuffer = new ComplexCircularBuffer(8);
}
/**
* Stores the complex sample in the buffer
*/
public void receive(float inphase, float quadrature)
{
mBuffer.put(new Complex(inphase, quadrature));
}
/**
* Stores the complex sample in the buffer
*/
public void receive(Complex complex)
{
mBuffer.put(complex.copy());
}
/**
* Sets the fractional sampling point for the current symbol to use in interpolating the current symbol.
*/
public void setSamplingPoint(float samplingPoint)
{
mSamplingPoint = samplingPoint;
}
/**
* Fractional sampling point
*/
public float getSamplingPoint()
{
return mSamplingPoint;
}
/**
* Raw samples
*/
public Complex[] getSamples()
{
return mBuffer.getAll();
}
public Complex[] getSamples(int length)
{
return mBuffer.get(length);
}
/**
* Array of (FM) demodulated samples representing the current symbol where each current sample is demodulated against
* the previous symbol's sample to produce the differential demodulated sample. Samples are in time order.
*
* Note: differential decoding is on an integral samples-per-symbol basis and does not use fractional interpolation.
*/
public Complex[] getDemodulated()
{
Complex[] samples = mBuffer.getAll();
int length = samples.length / 2;
Complex[] demodulated = new Complex[length];
for(int x = 0; x < samples.length / 2; x++)
{
Complex copy = samples[length + x].copy();
copy.multiply(samples[x].conjugate());
demodulated[x] = copy;
}
return demodulated;
}
}

View File

@@ -1,37 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.psk;
import org.apache.commons.math3.util.FastMath;
public enum SymbolInversion
{
DEGREES_90_PLUS(FastMath.PI / 2.0),
DEGREES_90_MINUS(FastMath.PI / -2.0),
DEGREES_180(FastMath.PI);
private double mAngleRadians;
SymbolInversion(double angleRadians)
{
mAngleRadians = angleRadians;
}
public double getError()
{
return mAngleRadians;
}
}

View File

@@ -0,0 +1,51 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.demod;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
/**
* Factory for creating PSK differential demodulators
*/
public class DifferentialDemodulatorFactory
{
/**
* Creates the optimal float output demodulator using calibration data to select the optimal implementation from
* scalar and vector options, or defaults to the scalar implementation if the calibration wasn't executed.
* @param sampleRate in Hertz
* @param symbolRate in Hertz
* @return demodulator instance
*/
public static DifferentialDemodulatorFloat getFloatDemodulator(double sampleRate, int symbolRate)
{
Implementation implementation = CalibrationManager.getInstance().getImplementation(CalibrationType.DIFFERENTIAL_DEMODULATOR);
return switch(implementation)
{
case VECTOR_SIMD_64 -> new DifferentialDemodulatorFloatVector64(sampleRate, symbolRate);
case VECTOR_SIMD_128 -> new DifferentialDemodulatorFloatVector128(sampleRate, symbolRate);
case VECTOR_SIMD_256 -> new DifferentialDemodulatorFloatVector256(sampleRate, symbolRate);
case VECTOR_SIMD_512 -> new DifferentialDemodulatorFloatVector512(sampleRate, symbolRate);
default -> new DifferentialDemodulatorFloatScalar(sampleRate, symbolRate); //Includes SCALAR case
};
}
}

View File

@@ -0,0 +1,85 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.demod;
import io.github.dsheirer.dsp.filter.interpolator.Interpolator;
import io.github.dsheirer.dsp.filter.interpolator.InterpolatorFactory;
import io.github.dsheirer.dsp.fm.IDemodulator;
/**
* Differential demodulator base class
*/
public abstract class DifferentialDemodulatorFloat implements IDemodulator
{
private int mSymbolRate;
private float mSampleRate;
private float mSamplesPerSymbol;
protected float[] mIBuffer = new float[20]; //Initial size 20 for array copy, but gets resized on first buffer
protected float[] mQBuffer = new float[20];
protected float mMu;
protected int mBufferOverlap;
protected int mInterpolationOffset;
protected final Interpolator mInterpolator = InterpolatorFactory.getInterpolator();
private DifferentialDemodulatorFloatScalar mScalar;
/**
* Constructor
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DifferentialDemodulatorFloat(double sampleRate, int symbolRate)
{
mSampleRate = (float) sampleRate;
mSymbolRate = symbolRate;
mSamplesPerSymbol = mSampleRate / mSymbolRate;
mMu = mSamplesPerSymbol % 1; //Fractional part of the samples per symbol rate
mInterpolationOffset = (int) Math.floor(mSamplesPerSymbol) - 4; //Interpolate at the middle of 8x samples
mBufferOverlap = (int) Math.floor(mSamplesPerSymbol) + 4;
while(mInterpolationOffset < 0)
{
mInterpolationOffset++;
mBufferOverlap++;
}
}
/**
* Scalar implementation for fallback when the sample buffer size is not compatible with the vector implementation.
* @return scalar implementation lazy instantiated when needed.
*/
protected DifferentialDemodulatorFloatScalar getScalarImplementation()
{
if(mScalar == null)
{
mScalar = new DifferentialDemodulatorFloatScalar(mSampleRate, mSymbolRate);
}
return mScalar;
}
/**
* Calculated samples per symbol.
* @return samples per symbol.
*/
public float getSamplesPerSymbol()
{
return mSamplesPerSymbol;
}
}

View File

@@ -0,0 +1,89 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Differential demodulator that uses scalar calculations for demodulating the sample stream.
*/
public class DifferentialDemodulatorFloatScalar extends DifferentialDemodulatorFloat
{
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatScalar.class);
/**
* Constructor
*
* @param symbolRate symbols per second
*/
public DifferentialDemodulatorFloatScalar(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}
@Override
public float[] demodulate(float[] i, float[] q)
{
int sampleLength = i.length;
int bufferOverlap = mBufferOverlap;
float mu = mMu;
//Copy previous buffer residual samples to beginning of buffer.
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
//Resize I/Q buffers if necessary
int requiredBufferLength = sampleLength + bufferOverlap;
if(mIBuffer.length != requiredBufferLength)
{
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
}
//Append new samples to the residual samples from the previous buffer.
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
float[] decodedPhases = new float[i.length];
float iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
//Differential demodulation.
for(int x = 0; x < sampleLength; x++)
{
iPrevious = mIBuffer[x];
qPreviousConjugate = -mQBuffer[x]; //Complex Conjugate
int offset = mInterpolationOffset + x;
iCurrent = mInterpolator.filter(mIBuffer, offset, mu);
qCurrent = mInterpolator.filter(mQBuffer, offset, mu);
//Multiply current complex sample by the complex conjugate of previous complex sample.
differentialI = (iPrevious * iCurrent) - (qPreviousConjugate * qCurrent);
differentialQ = (iPrevious * qCurrent) + (iCurrent * qPreviousConjugate);
decodedPhases[x] = (float)Math.atan2(differentialQ, differentialI);
}
return decodedPhases;
}
}

View File

@@ -0,0 +1,109 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Differential demodulator that uses Vector SIMD 128 calculations for demodulating the sample stream.
*/
public class DifferentialDemodulatorFloatVector128 extends DifferentialDemodulatorFloat
{
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatVector128.class);
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
/**
* Constructor
*
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DifferentialDemodulatorFloatVector128(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}
@Override
public float[] demodulate(float[] i, float[] q)
{
if(i.length % VECTOR_SPECIES.length() != 0)
{
return getScalarImplementation().demodulate(i,q);
}
int sampleLength = i.length;
int bufferOverlap = mBufferOverlap;
float mu = mMu;
//Copy previous buffer residual samples to beginning of buffer.
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
//Resize I/Q buffers if necessary
int requiredBufferLength = sampleLength + bufferOverlap;
if(mIBuffer.length != requiredBufferLength)
{
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
}
//Append new samples to the residual samples from the previous buffer.
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
float[] decodedPhases = new float[i.length];
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
//Differential demodulation.
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
{
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
int offset = mInterpolationOffset + x;
int index;
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
{
index = offset + y;
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
}
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
//Multiply current complex sample by the complex conjugate of previous complex sample.
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
}
return decodedPhases;
}
}

View File

@@ -0,0 +1,105 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Differential demodulator that uses Vector SIMD calculations for demodulating the sample stream.
*/
public class DifferentialDemodulatorFloatVector256 extends DifferentialDemodulatorFloat
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
/**
* Constructor
*
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DifferentialDemodulatorFloatVector256(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}
@Override
public float[] demodulate(float[] i, float[] q)
{
if(i.length % VECTOR_SPECIES.length() != 0)
{
return getScalarImplementation().demodulate(i,q);
}
int sampleLength = i.length;
int bufferOverlap = mBufferOverlap;
float mu = mMu;
//Copy previous buffer residual samples to beginning of buffer.
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
//Resize I/Q buffers if necessary
int requiredBufferLength = sampleLength + bufferOverlap;
if(mIBuffer.length != requiredBufferLength)
{
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
}
//Append new samples to the residual samples from the previous buffer.
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
float[] decodedPhases = new float[sampleLength];
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
//Differential demodulation.
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
{
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
int offset = mInterpolationOffset + x;
int index;
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
{
index = offset + y;
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
}
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
//Multiply current complex sample by the complex conjugate of previous complex sample.
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
}
return decodedPhases;
}
}

View File

@@ -0,0 +1,107 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Differential demodulator that uses Vector SIMD 512 calculations for demodulating the sample stream.
*/
public class DifferentialDemodulatorFloatVector512 extends DifferentialDemodulatorFloat
{
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatVector512.class);
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
/**
* Constructor
*
* @param symbolRate symbols per second
*/
public DifferentialDemodulatorFloatVector512(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}
@Override
public float[] demodulate(float[] i, float[] q)
{
if(i.length % VECTOR_SPECIES.length() != 0)
{
return getScalarImplementation().demodulate(i,q);
}
int sampleLength = i.length;
int bufferOverlap = mBufferOverlap;
float mu = mMu;
//Copy previous buffer residual samples to beginning of buffer.
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
//Resize I/Q buffers if necessary
int requiredBufferLength = sampleLength + bufferOverlap;
if(mIBuffer.length != requiredBufferLength)
{
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
}
//Append new samples to the residual samples from the previous buffer.
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
float[] decodedPhases = new float[sampleLength];
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
//Differential demodulation.
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
{
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
int offset = mInterpolationOffset + x;
int index;
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
{
index = offset + y;
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
}
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
//Multiply current complex sample by the complex conjugate of previous complex sample.
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
}
return decodedPhases;
}
}

View File

@@ -0,0 +1,108 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.psk.demod;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Differential demodulator that uses Vector SIMD 64 calculations for demodulating the sample stream.
*/
public class DifferentialDemodulatorFloatVector64 extends DifferentialDemodulatorFloat
{
private static final Logger LOGGER = LoggerFactory.getLogger(DifferentialDemodulatorFloatVector64.class);
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
/**
* Constructor
*
* @param sampleRate in Hertz
* @param symbolRate symbols per second
*/
public DifferentialDemodulatorFloatVector64(double sampleRate, int symbolRate)
{
super(sampleRate, symbolRate);
}
@Override
public float[] demodulate(float[] i, float[] q)
{
int sampleLength = i.length;
int bufferOverlap = mBufferOverlap;
float mu = mMu;
//Copy previous buffer residual samples to beginning of buffer.
System.arraycopy(mIBuffer, mIBuffer.length - bufferOverlap, mIBuffer, 0, bufferOverlap);
System.arraycopy(mQBuffer, mQBuffer.length - bufferOverlap, mQBuffer, 0, bufferOverlap);
//Resize I/Q buffers if necessary
int requiredBufferLength = sampleLength + bufferOverlap;
if(mIBuffer.length != requiredBufferLength)
{
if(i.length % VECTOR_SPECIES.length() != 0)
{
throw new IllegalArgumentException("Buffer length must be a multiple of " + VECTOR_SPECIES.length());
}
mIBuffer = Arrays.copyOf(mIBuffer, requiredBufferLength);
mQBuffer = Arrays.copyOf(mQBuffer, requiredBufferLength);
}
//Append new samples to the residual samples from the previous buffer.
System.arraycopy(i, 0, mIBuffer, bufferOverlap, sampleLength);
System.arraycopy(q, 0, mQBuffer, bufferOverlap, sampleLength);
//mIDecoded, mQDecoded and mPhase will be filled below during the decoding process.
float[] interpolatedI = new float[VECTOR_SPECIES.length()];
float[] interpolatedQ = new float[VECTOR_SPECIES.length()];
float[] decodedPhases = new float[i.length];
FloatVector iPrevious, qPreviousConjugate, iCurrent, qCurrent, differentialI, differentialQ;
//Differential demodulation.
for(int x = 0; x < sampleLength; x += VECTOR_SPECIES.length())
{
iPrevious = FloatVector.fromArray(VECTOR_SPECIES, mIBuffer, x);
qPreviousConjugate = FloatVector.fromArray(VECTOR_SPECIES, mQBuffer, x).neg(); //Complex Conjugate
int offset = mInterpolationOffset + x;
int index;
for(int y = 0; y < VECTOR_SPECIES.length(); y++)
{
index = offset + y;
interpolatedI[y] = mInterpolator.filter(mIBuffer, index, mu);
interpolatedQ[y] = mInterpolator.filter(mQBuffer, index, mu);
}
iCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedI, 0);
qCurrent = FloatVector.fromArray(VECTOR_SPECIES, interpolatedQ, 0);
//Multiply current complex sample by the complex conjugate of previous complex sample.
differentialI = iPrevious.mul(iCurrent).sub(qPreviousConjugate.mul(qCurrent));
differentialQ = iPrevious.mul(qCurrent).add(iCurrent.mul(qPreviousConjugate));
//Calculate phase angles using Arc Tangent and export to the decoded phases array.
differentialQ.lanewise(VectorOperators.ATAN2, differentialI).intoArray(decodedPhases, x);
}
return decodedPhases;
}
}

View File

@@ -1,125 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.module.decode.FeedbackDecoder;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Monitors decode sync events to adaptively control PLL loop gain.
*/
public class AdaptivePLLBandwidthMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(AdaptivePLLBandwidthMonitor.class);
private static final int MAX_SYNC_COUNT = PLLBandwidth.BW_200.getRangeEnd();
private CostasLoop mCostasLoop;
private FeedbackDecoder mFeedbackDecoder;
private PLLBandwidth mPLLBandwidth = PLLBandwidth.BW_400;
private int mSyncCount;
/**
* Constructs an adaptive monitor to monitor the sync state of a decoder and adjust the gain
* level of the costas loop accordingly.
*
* @param costasLoop to receive adaptive gain updates.
* @param feedbackDecoder to rebroadcast frequency error source events
*/
public AdaptivePLLBandwidthMonitor(CostasLoop costasLoop, FeedbackDecoder feedbackDecoder)
{
mCostasLoop = costasLoop;
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
mCostasLoop.setFrequencyErrorProcessor(this);
mFeedbackDecoder = feedbackDecoder;
}
/**
* Sync detection event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncDetected(int bitErrors)
{
mSyncCount++;
update();
}
/**
* Sync loss event. Updates the running sync count and updates the PLL gain level.
*/
@Override
public void syncLost(int bitsProcessed)
{
mSyncCount -= 2;
update();
}
/**
* Updates the gain state for a sync detect or sync loss
*/
private void update()
{
if(mSyncCount < 0)
{
mSyncCount = 0;
return;
}
if(mSyncCount > MAX_SYNC_COUNT)
{
mSyncCount = MAX_SYNC_COUNT;
return;
}
PLLBandwidth gain = PLLBandwidth.fromSyncCount(mSyncCount);
//Only update the costas loop if the gain value changes
if(gain != mPLLBandwidth)
{
mPLLBandwidth = gain;
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
}
}
/**
* Resets the monitor and commands the PLL to use the highest gain level.
*/
public void reset()
{
mSyncCount = 0;
mPLLBandwidth = PLLBandwidth.BW_400;
mCostasLoop.setPLLBandwidth(mPLLBandwidth);
}
@Override
public void processFrequencyError(long frequencyError)
{
//Only rebroadcast the costas loop frequency error if the gain level has progressed above
//a bandwidth of 300, meaning that we have received at least 4 sync events
if(mPLLBandwidth != PLLBandwidth.BW_400 && mPLLBandwidth != PLLBandwidth.BW_300)
{
mFeedbackDecoder.broadcast(SourceEvent.frequencyErrorMeasurement(frequencyError));
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
/**
* Monitors decode sync events to adaptively control frequency correction broadcasts.
*/
@Deprecated //Remove after P25P2 decoder gets updated
public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFrequencyErrorProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(FrequencyCorrectionSyncMonitor.class);
@@ -98,7 +99,7 @@ public class FrequencyCorrectionSyncMonitor implements ISyncDetectListener, IFre
@Override
public void processFrequencyError(long frequencyError)
{
mFeedbackDecoder.broadcast(SourceEvent.pllFrequencyMeasurement(frequencyError));
mFeedbackDecoder.broadcast(SourceEvent.carrierOffsetMeasurement(-frequencyError));
//Only rebroadcast as a frequency error measurement if the sync count is more than 2
if(mSyncCount > 2)

View File

@@ -1,133 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.dsp.psk.pll;
import io.github.dsheirer.dsp.symbol.Dibit;
/**
* Utility for calculating phase inverted sync pattern derivatives for QPSK constellations
*/
public class QPSKPhaseInversionCalculator
{
public static void main(String[] args)
{
//DMR sync patterns
// String[] syncs = {"755FD7DF75F7", "DFF57D75DF5D", "7F7D5DD57DFD", "D5D7F77FD757", "77D55F7DFD77",
// "5D577F7757FF", "F7FDD5DDFD55", "7DFFD5F55D5F", "D7557F5FF7F5", "DD7FF5D757DD"};
//P25 Phase 2 sync pattern
String[] syncs = {"575D57F7FF"};
//P25 Phase 1 sync pattern
// String[] syncs = {"5575F5FF77FF"};
for(String sync: syncs)
{
StringBuilder sbNorm = new StringBuilder();
StringBuilder sbP90 = new StringBuilder();
StringBuilder sbM90 = new StringBuilder();
StringBuilder sb180 = new StringBuilder();
for(char letter: sync.toCharArray())
{
int value;
if(Character.isAlphabetic(letter))
{
value = letter - 55;
}
else
{
value = letter - 48;
}
sbNorm.append(letter);
sbP90.append(rotate(value, Dibit.Rotation.PLUS90));
sbM90.append(rotate(value, Dibit.Rotation.MINUS90));
sb180.append(rotate(value, Dibit.Rotation.INVERT));
}
System.out.println("NORMAL: " + sync);
System.out.println(" +90: " + sbP90);
System.out.println(" -90: " + sbM90);
System.out.println(" 180: " + sb180);
}
}
/**
* Converts the hexadecimal value to an integer, decomposes it into high and low order dibits, rotates the dibits
* in the specified direction, and recombines the rotated dibits into a 4-bit value, returned as a hex character.
* @param value to rotate
* @param direction direction
* @return rotated hex character
*/
public static String rotate(int value, Dibit.Rotation direction)
{
try
{
Dibit dibitHigh = getHighDibit(value);
Dibit dibitLow = getLowDibit(value);
return getHex(dibitHigh.rotate(direction), dibitLow.rotate(direction));
}
catch(Exception e)
{
//ignore
}
return "-";
}
/**
* Combines the bit values from the two dibits and converts the integer value to a hex character
* @param d1 containing the high order bits
* @param d2 containing the low order bits
* @return string hex character representing the value of these dibits combined
*/
public static String getHex(Dibit d1, Dibit d2)
{
int value = d1.getHighValue() + d2.getLowValue();
return Integer.toHexString(value).toUpperCase();
}
/**
* Calculates the dibit that represents the bits in the 0x8 and 0x4 bit positions of the value
* @param value
* @return a dibit
*/
public static Dibit getHighDibit(int value)
{
return Dibit.fromValue((value >> 2) & 3);
}
/**
* Calculates the dibit that represents the bits in the 0x2 and 0x1 bit positions of the value
* @param value
* @return a dibit
*/
public static Dibit getLowDibit(int value)
{
return Dibit.fromValue(value & 3);
}
}

View File

@@ -0,0 +1,54 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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.squelch;
import io.github.dsheirer.sample.Listener;
/**
* Interface for a decoder to expose access to noise squelch controls and status registration endpoint(s).
*/
public interface INoiseSquelchController
{
/**
* Registers the listener to receive noise squelch states every 10 milliseconds for UI display.
* @param listener to receive states or pass null to de-register a listener.
*/
void setNoiseSquelchStateListener(Listener<NoiseSquelchState> listener);
/**
* Sets the open and close noise thresholds in range 0.0 to 1.0, recommend: 0.1 open and 0.2 close
* @param open for the open noise variance calculation.
* @param close for the close noise variance calculation.
*/
void setNoiseThreshold(float open, float close);
/**
* Sets the hysteresis threshold in units of 10 milliseconds
* @param open in range 1-10, recommend: 4
* @param close in range 1-10, recommend: 6
*/
void setHysteresisThreshold(int open, int close);
/**
* Manual override of the squelch control to set an always un-squelch state (true) or automatic control (false).
* @param override (true) or (false) to turn off squelch override.
*/
void setSquelchOverride(boolean override);
}

View File

@@ -0,0 +1,529 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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.squelch;
import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.dsp.window.WindowType;
import io.github.dsheirer.sample.Listener;
import java.util.Arrays;
/**
* Squelch processor for unfiltered, demodulated audio that operators by high-pass filtering the audio to remove below
* 3,000 Hertz and processes 10-millisecond audio buffers to calculate the noise variance across the samples. This
* variance is compared to a noise threshold and uses time hysteresis in the form of consecutive buffers to toggle the
* squelch open or closed. Uses a delay buffer equal in length to the hysteresis plus two which enables locating the
* precise transition point of squelch/un-squelch and passing the audio of the detected signal while blocking the audio
* when there is no signal detected. Register an audio listener to receive squelch controlled audio and a squelch state
* listener to receive squelch toggle indications.
*/
public class NoiseSquelch implements INoiseSquelchController
{
public static final int DEFAULT_HYSTERESIS_OPEN_THRESHOLD = 4;
public static final int DEFAULT_HYSTERESIS_CLOSE_THRESHOLD = 6;
public static final int MINIMUM_HYSTERESIS_THRESHOLD = 1;
public static final int MAXIMUM_HYSTERESIS_THRESHOLD = 10;
public static final int VARIANCE_CALCULATION_WINDOW_MILLISECONDS = 10;
public static final float DEFAULT_NOISE_OPEN_THRESHOLD = 0.1f;
public static final float DEFAULT_NOISE_CLOSE_THRESHOLD = 0.19f;
public static final float MINIMUM_NOISE_THRESHOLD = 0.1f;
public static final float MAXIMUM_NOISE_THRESHOLD = 0.5f;
private float[] mFilteredBuffer = new float[0];
private float[] mAudioBuffer = new float[0];
private float mMeanAccumulator;
private float mNoiseOpenThreshold = DEFAULT_NOISE_OPEN_THRESHOLD;
private float mNoiseCloseThreshold = DEFAULT_NOISE_CLOSE_THRESHOLD;
private boolean mSquelch = true;
private boolean mSquelchOverride = false;
private int mMeanAccumulatorPointer;
private int mVarianceWindowSize;
private int mHysteresisOpenThreshold = DEFAULT_HYSTERESIS_OPEN_THRESHOLD;
private int mHysteresisCloseThreshold = DEFAULT_HYSTERESIS_CLOSE_THRESHOLD;
private int mHysteresisCount = 0;
private int mSquelchStateBroadcastCounter = 0;
private int mSquelchOpenIndex = 0;
private int mAudioBufferFilterDelay;
private IRealFilter mHighPassFilter;
private Listener<float[]> mAudioListener;
private Listener<SquelchState> mSquelchStateListener;
private Listener<NoiseSquelchState> mNoiseSquelchStateListener;
/**
* Constructs an instance
* @param noiseOpen in range 0.0 to 1.0 - recommend 0.1 to 0.2 (units of demodulated, high-pass filtered audio
* sample variance where the high-pass filtered audio samples range -1.0 to 1.0 and maximum variance is half of that
* range: 1.0)
* @param hysteresisOpen in range 1 to 10 controls the squelch toggle hysteresis - recommend 4 (units of 10 ms)
*/
public NoiseSquelch(float noiseOpen, float noiseClose, int hysteresisOpen, int hysteresisClose)
{
setNoiseThreshold(noiseOpen, noiseClose);
setHysteresisThreshold(hysteresisOpen, hysteresisClose);
}
/**
* Indicates if the current state is squelched.
* @return true if squelched and false if not squelched.
*/
public boolean isSquelched()
{
return mSquelch;
}
/**
* Sets the manual squelch override.
* @param override (true) or (false) to turn off manual squelch override.
*/
@Override
public void setSquelchOverride(boolean override)
{
mSquelchOverride = override;
if(mSquelchOverride)
{
broadcast(SquelchState.UNSQUELCH);
}
}
/**
* Sets the noise thresholds
* @param open in range 0.0-1.0 and less than or equal to close, recommend 0.1
* @param close in range 0.0-1.0 and greater than or equal to open, recommend 0.2
*/
public void setNoiseThreshold(float open, float close)
{
if(open < MINIMUM_NOISE_THRESHOLD || close > MAXIMUM_NOISE_THRESHOLD || open > close)
{
throw new IllegalArgumentException("Noise thresholds open/close [" + open + "/" + close + "] - open must be less than or equal to close and both in range " + MINIMUM_NOISE_THRESHOLD + "-" + MAXIMUM_NOISE_THRESHOLD);
}
mNoiseOpenThreshold = open;
mNoiseCloseThreshold = close;
}
/**
* Updates the hysteresis thresholds
* @param open in range 1 to 10 and less than or equal to close, recommend 4.
* @param close in range 1 to 10 and greater than or equal to open, recommend 6.
*/
public void setHysteresisThreshold(int open, int close)
{
if(open < MINIMUM_HYSTERESIS_THRESHOLD || close > MAXIMUM_HYSTERESIS_THRESHOLD || open > close)
{
throw new IllegalArgumentException("Hysteresis threshold open [" + open + "] must be less than or equal to close [" +
close + "] and both in range " + MINIMUM_HYSTERESIS_THRESHOLD + "-" + MAXIMUM_HYSTERESIS_THRESHOLD);
}
mHysteresisOpenThreshold = open;
mHysteresisCloseThreshold = close;
mHysteresisCount = Math.min(mHysteresisCloseThreshold, mHysteresisCount);
}
/**
* Registers the listener to receive noise squelch state updates every 100 milliseconds.
* @param listener to register, or de-register by passing in a null.
*/
public void setNoiseSquelchStateListener(Listener<NoiseSquelchState> listener)
{
mNoiseSquelchStateListener = listener;
}
/**
* Registers the listener to receive squelch state indications. Squelch state events will be sent to the listener
* in concert with processed audio buffers. An un-squelch event is followed by one or more audio buffers and then a
* squelch event.
* @param listener to register or de-register by passing in a null.
*/
public void setSquelchStateListener(Listener<SquelchState> listener)
{
mSquelchStateListener = listener;
}
/**
* Registers the listener to receive processed audio buffers in between un-squelch/squelch events.
* @param listener to register or de-register by passing in a null.
*/
public void setAudioListener(Listener<float[]> listener)
{
mAudioListener = listener;
}
/**
* Broadcasts the current noise squelch state to an optionally registered listener. This can be use used in the
* user interface to display the state of the channel and noise squelch activity.
* @param noise variance from the current 10-millisecond audio buffer.
*/
private void broadcastNoiseSquelchState(float noise)
{
if(mNoiseSquelchStateListener != null)
{
mNoiseSquelchStateListener.receive(new NoiseSquelchState(mSquelch, mSquelchOverride, noise,
mNoiseOpenThreshold, mNoiseCloseThreshold, mHysteresisCount, mHysteresisOpenThreshold,
mHysteresisCloseThreshold));
}
}
/**
* Broadcasts from the unfiltered audio buffer to an optionally registered audio listener. Note: the start and end
* indices are pointers to the filtered audio buffer, and we dispatch from the unfiltered audio buffer which is offset
* by half the length of the high-pass audio filter coefficients. This method adjusts the index arguments by this
* offset value to align the unfiltered audio with the high-pass filtered audio detection boundaries.
*
* @param start index of the filtered audio buffer
* @param end index of the filtered audio buffer
*/
private void broadcast(int start, int end)
{
if(mAudioListener != null)
{
mAudioListener.receive(Arrays.copyOfRange(mAudioBuffer, start, end));
}
}
/**
* Broadcasts the squelch state change to an optionally registered squelch state listener.
* @param squelchState to dispatch.
*/
private void broadcast(SquelchState squelchState)
{
//Override squelch state when we're in manual override
if(squelchState == SquelchState.SQUELCH && mSquelchOverride)
{
squelchState = SquelchState.UNSQUELCH;
}
if(mSquelchStateListener != null)
{
mSquelchStateListener.receive(squelchState);
}
}
/**
* Sets the sample rate for the incoming demodulated audio sample stream.
* @param sampleRate of the sample stream.
*/
public void setSampleRate(double sampleRate)
{
float[] coefficients = FilterFactory.getHighPass((int)sampleRate, 3000, 31, WindowType.BLACKMAN_HARRIS_7);
mHighPassFilter = new RealFIRFilter(coefficients);
mVarianceWindowSize = (int)(sampleRate * (VARIANCE_CALCULATION_WINDOW_MILLISECONDS / 1000.0));
//Offset for the filter delay between the audio buffer and the filtered audio noise detection buffer.
mAudioBufferFilterDelay = (int)Math.ceil(coefficients.length / 2.0);
}
public void process(float[] samples)
{
float[] filtered = mHighPassFilter.filter(samples);
int requiredLength = filtered.length + (mVarianceWindowSize * (mHysteresisCloseThreshold + 2));
//Resize the delay buffers if the incoming sample buffer size changes.
if(mFilteredBuffer.length != requiredLength)
{
mFilteredBuffer = Arrays.copyOf(mFilteredBuffer, requiredLength);
//The audio buffer is longer to account for filter delay of the high pass filter and to align the indices
//across the filtered and audio buffers.
mAudioBuffer = Arrays.copyOf(mAudioBuffer, requiredLength + mAudioBufferFilterDelay);
}
//Shuffle the samples to the left in the delay buffers and copy in the new samples.
int length = mFilteredBuffer.length - filtered.length;
System.arraycopy(mFilteredBuffer, filtered.length, mFilteredBuffer, 0, length);
System.arraycopy(filtered, 0, mFilteredBuffer, length, filtered.length);
length = mAudioBuffer.length - samples.length;
System.arraycopy(mAudioBuffer, samples.length, mAudioBuffer, 0, length);
System.arraycopy(samples, 0, mAudioBuffer, length, samples.length);
int squelchCloseIndex;
//Adjust the squelch open index in sync with the left-shuffle of the audio samples.
mSquelchOpenIndex = Math.max(mSquelchOpenIndex - samples.length, 0);
//Process the newly added (filtered) samples in the delay buffer.
for(int x = mFilteredBuffer.length - filtered.length; x < mFilteredBuffer.length; x++)
{
mMeanAccumulator += mFilteredBuffer[x];
mMeanAccumulatorPointer++;
if(mMeanAccumulatorPointer == mVarianceWindowSize)
{
float mean = mMeanAccumulator / mVarianceWindowSize;
//Reset averaging variables
mMeanAccumulator = 0.0f;
mMeanAccumulatorPointer = 0;
//Calculate the variance
float varianceAccumulator1 = 0.0f, deviation, varianceAccumulator2 = 0.0f;
for(int i = x - mVarianceWindowSize + 1; i <= x; i++)
{
deviation = mFilteredBuffer[i] - mean;
varianceAccumulator1 += deviation * deviation;
varianceAccumulator2 += deviation;
}
//Formula from Apache Commons Math - Variance class.
float noiseVariance = (varianceAccumulator1 - (varianceAccumulator2 * varianceAccumulator2 / mVarianceWindowSize)) / mVarianceWindowSize;
boolean below = mSquelch ? (noiseVariance < mNoiseOpenThreshold) : (noiseVariance < mNoiseCloseThreshold);
mHysteresisCount += (below ? 1 : -1);
if(mSquelch && mHysteresisCount >= mHysteresisOpenThreshold)
{
mSquelch = false;
mHysteresisCount = mHysteresisOpenThreshold;
mSquelchOpenIndex = findTransition(x);
if(!mSquelchOverride)
{
broadcast(SquelchState.UNSQUELCH);
}
}
else if(!mSquelch && mHysteresisCount <= 0)
{
mSquelch = true;
mHysteresisCount = 0;
squelchCloseIndex = findTransition(x);
//Broadcast the partial audio segment ending at the transition point.
if(mSquelchOpenIndex < squelchCloseIndex)
{
//Only broadcast the truncated audio samples when we're not in squelch override
if(!mSquelchOverride)
{
broadcast(mSquelchOpenIndex, squelchCloseIndex);
}
broadcast(SquelchState.SQUELCH);
}
mSquelchOpenIndex = 0;
}
else if(mSquelch && !mSquelchOverride)
{
int start = x - (mVarianceWindowSize * mHysteresisOpenThreshold) + 1;
int end = start + mVarianceWindowSize;
//Make sure start doesn't go negative
start = Math.max(0, start);
Arrays.fill(mAudioBuffer, start, end, 0.0f);
}
mHysteresisCount = Math.min(mHysteresisCount, mHysteresisCloseThreshold);
mHysteresisCount = Math.max(mHysteresisCount, 0);
//Broadcast squelch state to an optionally registered listener (the channel tab in user interface)
mSquelchStateBroadcastCounter++;
if(mSquelchStateBroadcastCounter >= 5)
{
broadcastNoiseSquelchState(noiseVariance);
mSquelchStateBroadcastCounter -= 5;
}
}
}
if(mSquelchOverride)
{
broadcast(0, samples.length);
}
else if(!mSquelch && mSquelchOpenIndex < (samples.length))
{
mSquelchOpenIndex = Math.max(mSquelchOpenIndex, 0);
//Dispatch audio after processing the audio buffer if we end in an un-squelch state.
broadcast(mSquelchOpenIndex, samples.length);
mSquelchOpenIndex = 0;
}
}
/**
* Identifies the squelch transition point across a hysteresis window delayed by 2x buffer periods from the current
* processing index. This enables fine-grained squelch control without losing audio during the un-squelch period or
* allowing noise to leak into the squelch period at the end of the transmission. Uses a reduced 20-sample variance
* calculation window extended to the hysteresis time period to detect the transition point. Identifies the
* transition point within +/- 20 audio samples.
*
* @param current buffer processing index.
* @return buffer index of detected transition.
*/
private int findTransition(int current)
{
//Search window is 10-ms buffer size extended to hysteresis period, delayed by 2x buffer lengths.
int window = mSquelch ? mHysteresisCloseThreshold : mHysteresisOpenThreshold;
//Thresholds here may seem reversed but keep in mind that the mSquelch state has already been flipped and thus
// represents the state moving forward, and we need to detect the transition from the previous mSquelch state.
float varianceThreshold = mSquelch ? mNoiseCloseThreshold : mNoiseOpenThreshold;
int hysteresisThreshold = mSquelch ? mHysteresisCloseThreshold : mHysteresisOpenThreshold;
//Look back window start and end indices are sized according the previous mSquelch state
int start = current - (mVarianceWindowSize * (window + 2));
int end = start + (mVarianceWindowSize * window);
int hysteresisCount = 0;
int windowSize = mVarianceWindowSize / 5; //Decrease transition variance window to 1/5th of the normal variance window
int transitionPointer = 0;
for(int x = start; x < end; x += windowSize)
{
float mean = getMean(x, x + windowSize);
float variance = getVariance(x, x + windowSize, mean);
//Detect transition from from un-squelch to squelch
boolean exceedsThreshold = (mSquelch ? (variance > varianceThreshold) : (variance < varianceThreshold));
if(exceedsThreshold)
{
if(hysteresisCount == 0)
{
hysteresisCount++;
transitionPointer = x;
}
else
{
hysteresisCount++;
if(hysteresisCount >= hysteresisThreshold)
{
return transitionPointer;
}
}
}
else
{
hysteresisCount--;
hysteresisCount = Math.max(0, hysteresisCount);
}
}
return 0;
}
/**
* Calculate the mean/average of values between the start and end indices.
* @param start index
* @param end index
* @return mean value.
*/
private float getMean(int start, int end)
{
if(start < 0)
{
start = 0;
}
if(end > mFilteredBuffer.length)
{
end = mFilteredBuffer.length;
}
float accumulator = 0.0f;
for(int x = start; x < end; x++)
{
accumulator += mFilteredBuffer[x];
}
return accumulator / (end - start);
}
/**
* Calculates the variance of the values between start and end indices using the supplied mean value.
* @param start index
* @param end index
* @param mean of values between start and end, calculated previously
* @return variance.
*/
private float getVariance(int start, int end, float mean)
{
if(start < 0)
{
start = 0;
}
if(end > mFilteredBuffer.length)
{
end = mFilteredBuffer.length;
}
float varianceAccumulator1 = 0.0f, deviation, varianceAccumulator2 = 0.0f;
for(int i = start; i < end; i++)
{
deviation = mFilteredBuffer[i] - mean;
varianceAccumulator1 += deviation * deviation;
varianceAccumulator2 += deviation;
}
int sampleSize = end - start;
//Formula from Apache Commons Math - Variance class.
return (varianceAccumulator1 - (varianceAccumulator2 * varianceAccumulator2 / sampleSize)) / sampleSize;
}
public static void main(String[] args)
{
// Path directory = Paths.get("/media/denny/T9/NBFM Squelch Research"); //Linux
// Path file = directory.resolve("20250629_072704_453850000_Onondaga-County_Fire-EMS_OC-Fire-Disp_79_baseband.wav");
// Path file = directory.resolve("DMR_1_CAPPLUS.wav");
// Path file = directory.resolve("20250702_034109_145510000_R828D_V4_POLY_No_Antenna_Normal_Gain.wav");
// Path file = directory.resolve("20250702_034301_145510000_R828D_V4_POLY_No_Antenna_Max_Gain.wav");
// Path file = directory.resolve("20250702_034403_145510000_R828D_V4_POLY_No_Antenna_Min_Gain.wav");
// Path file = directory.resolve("20250702_034541_145510000_R828D_V4_HET_No_Antenna_Min_Gain.wav");
// Path file = directory.resolve("20250702_034635_145510000_R828D_V4_HET_No_Antenna_Max_Gain.wav");
// Path file = directory.resolve("20250702_034742_145510000_R828D_V4_HET_No_Antenna_Normal_Gain.wav");
// Path file = directory.resolve("20250703_043912_162500000_NOAA_25_khz_no_center_signal.wav");
// Path file = directory.resolve("20250703_043958_162550000_NOAA_25_khz_yes_center_signal.wav");
//
// DecodeConfigNBFM config = new DecodeConfigNBFM();
// config.setBandwidth(DecodeConfigAnalog.Bandwidth.BW_12_5);
// NBFMDecoder decoder = new NBFMDecoder(config);
//
// try(ComplexWaveSource source = new ComplexWaveSource(file.toFile(), false))
// {
// source.setListener(iNativeBuffer -> {
// Iterator<ComplexSamples> it = iNativeBuffer.iterator();
//
// while(it.hasNext())
// {
// ComplexSamples samples = it.next();
// decoder.receive(samples);
// }
// });
// source.start();
//
// decoder.getSourceEventListener().receive(SourceEvent.sampleRateChange(source.getSampleRate()));
//
// while(true)
// {
// source.next(2048, true);
// }
// }
// catch(IOException ioe)
// {
//
// ioe.printStackTrace();
// }
//
// System.out.println("Finished");
}
}

View File

@@ -0,0 +1,35 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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.squelch;
/**
* Noise Squelch operating status record. Captures a snapshot in time of the squelch status.
* @param squelch state, on (true) or off (false)
* @param squelchOverride on (true) or off (false)
* @param noise - current noise variance
* @param noiseOpenThreshold for determining un-squelch state
* @param noiseCloseThreshold for determining squelch state
* @param hysteresis in units of 10-milliseconds
* @param hysteresisOpenThreshold for toggling the squelch state.
* @param hysteresisCloseThreshold for toggling the squelch state.
*/
public record NoiseSquelchState(boolean squelch, boolean squelchOverride, float noise, float noiseOpenThreshold,
float noiseCloseThreshold, int hysteresis, int hysteresisOpenThreshold,
int hysteresisCloseThreshold){}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -34,14 +34,14 @@ public class PowerMonitor
private int mPowerLevelBroadcastCount = 0;
private int mPowerLevelBroadcastThreshold;
private Listener<SourceEvent> mSourceEventListener;
private SinglePoleIirFilter mPowerFilter = new SinglePoleIirFilter(0.1f);
private final SinglePoleIirFilter mPowerFilter = new SinglePoleIirFilter(0.1f);
/**
* Constructs an instance
*/
public PowerMonitor()
{
mPowerLevelBroadcastThreshold = 25000; //Based on a default sample rate of 50 kHz
mPowerLevelBroadcastThreshold = 12500; //Based on a default sample rate of 25 kHz
}
/**
@@ -54,36 +54,25 @@ public class PowerMonitor
mPowerLevelBroadcastThreshold = sampleRate / 2;
}
/**
* Processes a complex IQ sample and changes squelch state when the signal power is above or below the
* threshold value.
* @param inphase complex sample component
* @param quadrature complex sample component
*/
public void process(float inphase, float quadrature)
{
mPowerLevelBroadcastCount++;
if(mPowerLevelBroadcastCount > mPowerLevelBroadcastThreshold)
{
mPowerFilter.filter(inphase * inphase + quadrature * quadrature);
}
if(mPowerLevelBroadcastCount > (mPowerLevelBroadcastThreshold + 10))
{
mPowerLevelBroadcastCount = 0;
broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPowerFilter.getValue())));
}
}
/**
* Processes I&Q complex baseband sample buffers.
*/
public void process(float[] i, float[] q)
{
for(int x = 0; x < i.length; x++)
mPowerLevelBroadcastCount += i.length;
if(mPowerLevelBroadcastCount > mPowerLevelBroadcastThreshold && i.length >= 10)
{
process(i[x], q[x]);
double iSquared, qSquared;
for(int x = 0; x < 10; x++)
{
iSquared = Math.pow(i[x], 2.0f);
qSquared = Math.pow(q[x], 2.0f);
mPowerFilter.filter((float)(iSquared + qSquared));
}
mPowerLevelBroadcastCount -= mPowerLevelBroadcastThreshold;
broadcast(SourceEvent.channelPowerLevel(null, 10.0 * Math.log10(mPowerFilter.getValue())));
}
}
@@ -98,7 +87,7 @@ public class PowerMonitor
/**
* Broadcasts the source event to an optional register listener
*/
private void broadcast(SourceEvent event)
public void broadcast(SourceEvent event)
{
if(mSourceEventListener != null)
{

View File

@@ -1,47 +1,55 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.dsp.symbol;
public enum Dibit
{
D01_PLUS_3(false, true, 1, 4, 1),
D00_PLUS_1(false, false, 0, 0, 0),
D10_MINUS_1(true, false, 2, 8, 2),
D11_MINUS_3(true, true, 3, 12, 3);
D01_PLUS_3(false, true, 1, 4, 1, (float)((Math.PI / 4) * 3)),
D00_PLUS_1(false, false, 0, 0, 0, (float)(Math.PI / 4)),
D10_MINUS_1(true, false, 2, 8, 2, (float)(Math.PI / -4)),
D11_MINUS_3(true, true, 3, 12, 3, (float)((Math.PI / 4) * -3));
private boolean mBit1;
private boolean mBit2;
private int mLowValue;
private int mHighValue;
private int mValue;
private float mPhase;
private Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value)
/**
* Dibit constructor.
* @param bit1 boolean value
* @param bit2 boolean value
* @param lowValue integer value of the low order bit
* @param highValue integer value of the high order bit
* @param value integer value combined of both bits.
* @param phase optimal phase value for this symbol when transmitted.
*/
Dibit(boolean bit1, boolean bit2, int lowValue, int highValue, int value, float phase)
{
mBit1 = bit1;
mBit2 = bit2;
mLowValue = lowValue;
mHighValue = highValue;
mValue = value;
mPhase = phase;
}
/**
@@ -52,6 +60,15 @@ public enum Dibit
return mValue;
}
/**
* Ideal phase angle when this symbol is transmitted.
* @return ideal phase angle in radians.
*/
public float getIdealPhase()
{
return mPhase;
}
/**
* Allowable transition state 1 for trellis coded modulation. Indicates
* the dibit that differs from this dibit in the LSB bit position.
@@ -248,15 +265,24 @@ public enum Dibit
}
}
public static void main(String[] args)
/**
* Number of bit errors between this dibit and the expected dibit argument.
* @param dibit to compare
* @return 0, 1 or 2 bit errors
*/
public int getBitErrorFrom(Dibit dibit)
{
byte a = (byte)0x1B;
int xor = getValue() ^ dibit.getValue();
Dibit d1 = Dibit.parse(a, 0);
Dibit d2 = Dibit.parse(a, 1);
Dibit d3 = Dibit.parse(a, 2);
Dibit d4 = Dibit.parse(a, 3);
if(xor == 0)
{
return 0;
}
else if(xor == 3)
{
return 2;
}
System.out.println("1: " + d1 + " 2:" + d2 + " 3:" + d3 + " 4:" + d4);
return 1;
}
}

View File

@@ -1,260 +0,0 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.dsp.symbol;
import io.github.dsheirer.bits.BitSetFullException;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Circular buffer for storing and accessing dibits.
*/
public class DibitDelayBuffer
{
private final static Logger mLog = LoggerFactory.getLogger(DibitDelayBuffer.class);
protected Dibit[] mBuffer;
protected int mPointer;
/**
* Constructs a dibit delay buffer of the specified length
*/
public DibitDelayBuffer(int length)
{
Validate.isTrue(length > 0, "Dibit buffer length must be greater than 0");
mBuffer = new Dibit[length];
reset();
}
/**
* Resets the buffer and preloads the buffer with all zeros
*/
public void reset()
{
for(int x = 0; x < mBuffer.length; x++)
{
mBuffer[x] = Dibit.D00_PLUS_1;
}
}
/**
* Length of this circular buffer
*/
public int length()
{
return mBuffer.length;
}
/**
* Returns the dibit at the specified index
* @param index to get
* @return dibit at the specified index
*/
public Dibit get(int index)
{
if(index < 0 || index >= mBuffer.length)
{
throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]");
}
return mBuffer[index];
}
/**
* Sets the dibit at the specified index
* @param index of the dibit
* @param dibit to load at the specified index
*/
public void set(int index, Dibit dibit)
{
if(index < 0 || index >= mBuffer.length)
{
throw new IllegalArgumentException("Index argument must be between 0 and buffer length [" + mBuffer.length + "]");
}
mBuffer[index] = dibit;
}
/**
* Returns an ordered view of the internal circular buffer contents.
*/
public Dibit[] getBuffer()
{
Dibit[] transferBuffer = new Dibit[mBuffer.length];
int transferBufferPointer = 0;
int bufferPointer = mPointer;
while(transferBufferPointer < transferBuffer.length)
{
transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++];
if(bufferPointer >= mBuffer.length)
{
bufferPointer = 0;
}
}
return transferBuffer;
}
/**
* Retrieves a view of the buffer from the starting index to the specified length. Note: if the requested
* length exceeds the length of this buffer, the contents will be unwrapped from the circular buffer and repeated.
* @param start of the view where 0 is the first element.
* @param length of the dibit view
* @return an array of dibits
*/
public Dibit[] getBuffer(int start, int length)
{
Dibit[] transferBuffer = new Dibit[length];
int transferBufferPointer = 0;
int bufferPointer = (mPointer + start) % mBuffer.length;
while(transferBufferPointer < transferBuffer.length)
{
transferBuffer[transferBufferPointer++] = mBuffer[bufferPointer++];
if(bufferPointer >= mBuffer.length)
{
bufferPointer = 0;
}
}
return transferBuffer;
}
/**
* Extracts a corrected binary message from the dibit buffer
*
* @param start dibit index where 0 is the oldest dibit and the newest dibit is buffer length - 1
* @param dibitLength number of dibits to include in the message, making the message length = dibitLength * 2
* @return corrected binary message
*/
public CorrectedBinaryMessage getMessage(int start, int dibitLength)
{
CorrectedBinaryMessage message = new CorrectedBinaryMessage(dibitLength * 2);
int dibitCount = 0;
int bufferPointer = (mPointer + start) % mBuffer.length;
try
{
while(dibitCount < dibitLength)
{
Dibit dibit = mBuffer[bufferPointer++];
message.add(dibit.getBit1());
message.add(dibit.getBit2());
dibitCount++;
if(bufferPointer >= mBuffer.length)
{
bufferPointer = 0;
}
}
}
catch(BitSetFullException e)
{
//This shouldn't happen
mLog.error("Bit set full ??", e);
}
return message;
}
/**
* Utility method to view the buffer contents with the dibits decoded as an array of zeros and ones.
*/
public int[] getBufferAsArray()
{
Dibit[] dibits = getBuffer();
int[] bits = new int[dibits.length * 2];
for(int x = 0; x < dibits.length; x++)
{
if(dibits[x].getBit1())
{
bits[x * 2] = 1;
}
if(dibits[x].getBit2())
{
bits[x * 2 + 1] = 1;
}
}
return bits;
}
/**
* Places the dibit into the internal circular buffer, overwriting the oldest dibit.
*/
public void put(Dibit dibit)
{
mBuffer[mPointer++] = dibit;
if(mPointer >= mBuffer.length)
{
mPointer = 0;
}
}
/**
* Places the dibit into the internal circular buffer, overwriting and returning the
* oldest dibit.
*/
public Dibit getAndPut(Dibit dibit)
{
Dibit toReturn = mBuffer[mPointer];
put(dibit);
return toReturn;
}
/**
* Logs the contents of the buffer
*/
public void log()
{
StringBuilder sb = new StringBuilder();
int counter = 0;
int pointer = mPointer;
while(counter < mBuffer.length)
{
sb.append(mBuffer[pointer].getBit1() ? "1" : "0");
sb.append(mBuffer[pointer++].getBit2() ? "1" : "0");
if(pointer >= mBuffer.length)
{
pointer = 0;
}
counter++;
}
mLog.debug("BUFFER: " + sb + " Length:" + mBuffer.length);
}
}

View File

@@ -0,0 +1,128 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.symbol;
import java.util.Arrays;
/**
* Implements a circular buffer delay line for dibits.
*/
public class DibitDelayLine
{
protected Dibit[] mDelayLine;
public int mPointer;
/**
* Constructs an instance
* @param length of the delay line in dibits.
*/
public DibitDelayLine(int length)
{
mDelayLine = new Dibit[length];
Arrays.fill(mDelayLine, Dibit.D00_PLUS_1);
}
/**
* Inserts the dibit into the delay line and returns the oldest dibit from the delay line.
* @param dibit to insert
* @return oldest dibit
*/
public Dibit insert(Dibit dibit)
{
Dibit ejected = mDelayLine[mPointer];
mDelayLine[mPointer++] = dibit;
mPointer %= mDelayLine.length;
return ejected;
}
/**
* Overwrites the most recently added dibits in the delay line with the provided sequence. This can be used when
* a sync pattern is detected to overwrite the calculated symbols with the known sync pattern.
* @param dibits to overwrite
*/
public void update(Dibit[] dibits)
{
mPointer = mPointer - dibits.length;
if(mPointer < 0)
{
mPointer += mDelayLine.length;
}
for(Dibit dibit: dibits)
{
mDelayLine[mPointer++] = dibit;
mPointer %= mDelayLine.length;
}
}
/**
* Adjusts the pointer to correct a dibit stuff/delete.
* @param offset to adjust.
*/
public void adjustPointer(int offset)
{
mPointer += offset;
if(mPointer < 0)
{
mPointer += mDelayLine.length;
}
else if(mPointer >= mDelayLine.length)
{
mPointer -= mDelayLine.length;
}
}
/**
* Returns the most recently stored Dibit.
*/
public Dibit getLast()
{
int pointer = mPointer - 1;
if(pointer < 0)
{
pointer += mDelayLine.length;
}
return mDelayLine[pointer];
}
public void log()
{
StringBuilder sb = new StringBuilder();
int pointer = mPointer;
for(int i = 0; i < mDelayLine.length; i++)
{
Dibit dibit = mDelayLine[pointer];
sb.append(dibit.getBit1() ? "1" : "0");
sb.append(dibit.getBit2() ? "1" : "0");
sb.append(" ");
pointer++;
pointer = pointer % mDelayLine.length;
}
System.out.println(sb);
}
}

View File

@@ -1,106 +0,0 @@
/*
* *****************************************************************************
* 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.edac;
import io.github.dsheirer.bits.BinaryMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BCH_63_16_11 extends ReedSolomon_63_P25
{
private final static Logger mLog = LoggerFactory.getLogger(BCH_63_16_11.class);
/**
* BCH( 63,16,11) decoder
*/
public BCH_63_16_11()
{
super(63, 41);
}
/**
* Performs error detection and correction on the first 63 bits of the
* message argument. If the message is correctable, only the first 16 bits
* (information bits) are corrected.
*
* @return - true = success, false = failure
*/
public BinaryMessage correctNID(BinaryMessage message)
{
CRC status = CRC.PASSED;
int[] original = message.toReverseIntegerArray(0, 62);
int[] corrected = new int[63];
boolean irrecoverableErrors = decode(original, corrected);
if(irrecoverableErrors)
{
message.setCRC(CRC.FAILED_CRC);
return message;
}
else
{
for(int x = 0; x < 16; x++)
{
int index = 63 - x - 1;
if(corrected[index] != original[index])
{
status = CRC.CORRECTED;
if(corrected[index] == 1)
{
message.set(x);
}
else
{
message.clear(x);
}
}
}
}
message.setCRC(status);
return message;
}
public static void main(String[] args)
{
String orig = "0010011000000011010010100000000110000111110011101010001010110000";
String error = "0001010100000011010010100000000110000111100011001010001010110000";
BinaryMessage errorMessage = BinaryMessage.load(error);
BCH_63_16_11 bch = new BCH_63_16_11();
mLog.debug("ORIG:" + orig);
mLog.debug(" ERR:" + errorMessage.toString());
mLog.debug(" ");
errorMessage = bch.correctNID(errorMessage);
mLog.debug(" CRC: " + errorMessage.getCRC().name());
mLog.debug("CORR:" + errorMessage.toString());
mLog.debug("ORIG:" + orig);
}
}

View File

@@ -1,62 +0,0 @@
package io.github.dsheirer.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
/*
Adopted from https://github.com/boatbod/op25/blob/e5e8fe457240fb358a8163bcf6fe7dda5e4d8392/op25/gr-op25_repeater/lib/hamming.cc
*/
public class BPTC_17_12_3 {
public static boolean decode17123(BinaryMessage d, int offset)
{
if(d==null) {
return false;
}
// Calculate the checksum this column should have
boolean c0 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(6 + offset) ^ d.get(7 + offset) ^ d.get(9 + offset);
boolean c1 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(7 + offset) ^ d.get(8 + offset) ^ d.get(10 + offset);
boolean c2 = d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(3 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(8 + offset) ^ d.get(9 + offset) ^ d.get(11 + offset);
boolean c3 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(4 + offset) ^ d.get(5 + offset) ^ d.get(7 + offset) ^ d.get(10 + offset);
boolean c4 = d.get(0 + offset) ^ d.get(1 + offset) ^ d.get(2 + offset) ^ d.get(5 + offset) ^ d.get(6 + offset) ^ d.get(8 + offset) ^ d.get(11 + offset);
// Compare these with the actual bits
byte n = 0x00;
n |= (c0 != d.get(12 + offset)) ? 0x01 : 0x00;
n |= (c1 != d.get(13 + offset)) ? 0x02 : 0x00;
n |= (c2 != d.get(14 + offset)) ? 0x04 : 0x00;
n |= (c3 != d.get(15 + offset)) ? 0x08 : 0x00;
n |= (c4 != d.get(16 + offset)) ? 0x10 : 0x00;
switch (n) {
// Parity bit errors
case 0x01: d.set(12 + offset, !d.get(12 + offset)); return true;
case 0x02: d.set(13 + offset, !d.get(13 + offset)); return true;
case 0x04: d.set(14 + offset, !d.get(14 + offset)); return true;
case 0x08: d.set(15 + offset, !d.get(15 + offset)); return true;
case 0x10: d.set(16 + offset, !d.get(16 + offset)); return true;
// Data bit errors
case 0x1B: d.set(0 + offset, !d.get(0 + offset)); return true;
case 0x1F: d.set(1 + offset, !d.get(1 + offset)); return true;
case 0x17: d.set(2 + offset, !d.get(2 + offset)); return true;
case 0x07: d.set(3 + offset, !d.get(3 + offset)); return true;
case 0x0E: d.set(4 + offset, !d.get(4 + offset)); return true;
case 0x1C: d.set(5 + offset, !d.get(5 + offset)); return true;
case 0x11: d.set(6 + offset, !d.get(6 + offset)); return true;
case 0x0B: d.set(7 + offset, !d.get(7 + offset)); return true;
case 0x16: d.set(8 + offset, !d.get(8 + offset)); return true;
case 0x05: d.set(9 + offset, !d.get(9 + offset)); return true;
case 0x0A: d.set(10 + offset,!d.get(10 + offset)); return true;
case 0x14: d.set(11 + offset,!d.get(11 + offset)); return true;
// No bit errors
case 0x00: return true;
// Unrecoverable errors
default: return false;
}
}
}

View File

@@ -1,307 +0,0 @@
/*
* *****************************************************************************
* 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.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.BitSetFullException;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Block Product Turbo Code (BPTC) 196/96 Support
*/
public class BPTC_196_96
{
public static final int BPTC_LENGTH = 196;
public static final int EXTRACTED_LENGTH = 96; //However, we set the 3x reserved bits in 96, 97, and 98 making the length 99
public static final int MAX_ORIGINAL_INDEX = 136;
public static final int COLUMN_COUNT = 15;
public static final int MESSAGE_COLUMN_COUNT = 12; //Should be 11, but adjusted for the first pad bit
public static final int CHECKSUM_COLUMN_COUNT = 4;
public static final int MESSAGE_START_INDEX = 4;
/**
* DMR De-interleaving indices
*/
public static int[] BPTC_DEINTERLEAVE = new int[]{0, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 16, 1, 182,
167, 152, 137, 122, 107, 92, 77, 62, 47, 32, 17, 2, 183, 168, 153, 138, 123, 108, 93, 78, 63, 48, 33, 18, 3,
184, 169, 154, 139, 124, 109, 94, 79, 64, 49, 34, 19, 4, 185, 170, 155, 140, 125, 110, 95, 80, 65, 50, 35, 20,
5, 186, 171, 156, 141, 126, 111, 96, 81, 66, 51, 36, 21, 6, 187, 172, 157, 142, 127, 112, 97, 82, 67, 52, 37,
22, 7, 188, 173, 158, 143, 128, 113, 98, 83, 68, 53, 38, 23, 8, 189, 174, 159, 144, 129, 114, 99, 84, 69, 54,
39, 24, 9, 190, 175, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, 191, 176, 161, 146, 131, 116, 101, 86, 71,
56, 41, 26, 11, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, 42, 27, 12, 193, 178, 163, 148, 133, 118, 103,
88, 73, 58, 43, 28, 13, 194, 179, 164, 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 195, 180, 165, 150, 135, 120,
105, 90, 75, 60, 45, 30, 15};
/**
* DMR Column Indices
*/
public static int[][] COLUMN_INDEXES = new int[][]{
{1, 16, 31, 46, 61, 76, 91, 106, 121, 136, 151, 166, 181},
{2, 17, 32, 47, 62, 77, 92, 107, 122, 137, 152, 167, 182},
{3, 18, 33, 48, 63, 78, 93, 108, 123, 138, 153, 168, 183},
{4, 19, 34, 49, 64, 79, 94, 109, 124, 139, 154, 169, 184},
{5, 20, 35, 50, 65, 80, 95, 110, 125, 140, 155, 170, 185},
{6, 21, 36, 51, 66, 81, 96, 111, 126, 141, 156, 171, 186},
{7, 22, 37, 52, 67, 82, 97, 112, 127, 142, 157, 172, 187},
{8, 23, 38, 53, 68, 83, 98, 113, 128, 143, 158, 173, 188},
{9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 159, 174, 189},
{10, 25, 40, 55, 70, 85, 100, 115, 130, 145, 160, 175, 190},
{11, 26, 41, 56, 71, 86, 101, 116, 131, 146, 161, 176, 191},
{12, 27, 42, 57, 72, 87, 102, 117, 132, 147, 162, 177, 192},
{13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193},
{14, 29, 44, 59, 74, 89, 104, 119, 134, 149, 164, 179, 194},
{15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195}};
/**
* Performs de-interleave, error detect and correct and extracts a 96-bit message from the DMR BPTC(196,96)
* protected message
* @param original uncorrected BPTC(196,96) message
* @return error corrected and extracted 96-bit message
*/
public static CorrectedBinaryMessage extract(CorrectedBinaryMessage original)
{
//De-Interleave
CorrectedBinaryMessage message = deinterleave(original);
//Perform error detection and correction
correct(message);
//Extract the 96-bit message from the corrected 196 bits
CorrectedBinaryMessage extracted = new CorrectedBinaryMessage(EXTRACTED_LENGTH);
//Transfer the corrected bit count
extracted.setCorrectedBitCount(message.getCorrectedBitCount());
int index = MESSAGE_START_INDEX;
while(index < MAX_ORIGINAL_INDEX)
{
if((index % COLUMN_COUNT) < MESSAGE_COLUMN_COUNT)
{
try
{
extracted.add(message.get(index));
}
catch(BitSetFullException bsfe)
{
//Should never get to here
bsfe.printStackTrace();
return extracted;
}
index++;
}
else
{
index += CHECKSUM_COLUMN_COUNT;
}
}
//Transfer bits R2, R1, and R0 to the end - RAS bits
extracted.set(96, message.get(0));
extracted.set(97, message.get(1));
extracted.set(98, message.get(2));
return extracted;
}
/**
* De-interleaves the transmitted bits of a DMR BPTC(196,96) protected message
* @param message that contains interleaved (transmitted) message
* @return de-interleaved message
*/
private static CorrectedBinaryMessage deinterleave(CorrectedBinaryMessage message)
{
CorrectedBinaryMessage deinterleaved = new CorrectedBinaryMessage(BPTC_LENGTH);
for(int x = 0; x < BPTC_DEINTERLEAVE.length; x++)
{
deinterleaved.set(x, message.get(BPTC_DEINTERLEAVE[x]));
}
return deinterleaved;
}
/**
* Identifies the error indices from the row level Hamming(15,11,3) protected words
* @param message containing protected words in rows
* @return list of zero or more error indices
*/
private static List<Integer> getRowErrors(CorrectedBinaryMessage message)
{
List<Integer> rowErrors = new ArrayList<>();
int offset = 0;
int index = -1;
for(int row = 0; row < 13; row++)
{
//The first bit of the message is a pad/reserved bit and is ignored.
offset = (row * 15) + 1;
index = Hamming15.getErrorIndex(message, offset);
if(index >= 0)
{
rowErrors.add(index);
}
}
//Note: row errors should already be in order and don't need sorted
return rowErrors;
}
/**
* Identifies the error indices from the column level Hamming(13,9,3) protected words
* @param message containing protected words in columns
* @return list of zero or more error indices
*/
private static List<Integer> getColumnErrors(CorrectedBinaryMessage message)
{
List<Integer> columnErrors = new ArrayList<>();
int index = -1;
for(int column = 0; column < 15; column++)
{
index = Hamming13.getErrorIndex(message, COLUMN_INDEXES[column]);
if(index >= 0)
{
columnErrors.add(index);
}
}
Collections.sort(columnErrors);
return columnErrors;
}
/**
* Performs error detection and correction on the DMR BPTC(196,96) protected message
*
* @param message to process and extract
*/
private static void correct(CorrectedBinaryMessage message)
{
List<Integer> rowErrors = getRowErrors(message);
if(!rowErrors.isEmpty())
{
List<Integer> columnErrors = getColumnErrors(message);
if(matches(rowErrors, columnErrors))
{
for(Integer index: rowErrors)
{
message.flip(index);
}
message.setCorrectedBitCount(rowErrors.size());
}
else
{
//Step 1: correct row errors and recalculate column errors (should be zero)
for(Integer index: rowErrors)
{
message.flip(index);
}
List<Integer> columnErrorsPart2 = getColumnErrors(message);
if(columnErrorsPart2.isEmpty())
{
//Success
message.setCorrectedBitCount(rowErrors.size());
}
else
{
//Reverse the row error correction
for(Integer index: rowErrors)
{
message.flip(index);
}
//Step 2: correct column errors and recalculate row errors (should be zero)
for(Integer index: columnErrors)
{
message.flip(index);
}
List<Integer> rowErrorsPart2 = getRowErrors(message);
if(rowErrorsPart2.isEmpty())
{
//Success
message.setCorrectedBitCount(columnErrors.size());
}
else
{
//Reverse the column error correction
for(Integer index: columnErrors)
{
message.flip(index);
}
//Set the error count greater than the maximum correctable column errors (16)
message.setCorrectedBitCount(16);
}
}
}
}
}
/**
* Indicates if the errors detected by the row protected words match the errors detected by the column protected words
* @param rowErrors bit positions
* @param columnErrors bit positions
* @return true if they have the same quantity of bit position errors and the bit positions match
*/
private static boolean matches(List<Integer> rowErrors, List<Integer> columnErrors)
{
if(rowErrors.size() == columnErrors.size())
{
for(int x = 0; x < rowErrors.size(); x++)
{
if(!rowErrors.get(x).equals(columnErrors.get(0)))
{
return false;
}
}
return true;
}
else
{
return false;
}
}
public static void main(String[] args)
{
String raw = "0000000001100010100101100011100100110100110101000100000011101001110001001000100001001010010100001000011111011000000011110000111000111111100011000011100001100000000101001010000100010111010100111001";
System.out.println("RAW:" + raw);
CorrectedBinaryMessage message = new CorrectedBinaryMessage(BinaryMessage.load(raw));
System.out.println("LOD:" + message.toString());
CorrectedBinaryMessage extracted = extract(message);
System.out.println("EXT:" + extracted.toString());
}
}

View File

@@ -0,0 +1,53 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This 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.edac;
import io.github.dsheirer.bits.BinaryMessage;
import io.github.dsheirer.bits.IntField;
/**
* Utility for calculating the CRC checksum for CRC-16 using polynomial 0x1021 and Initial Fill/Residual of 0xFFFF
*/
public class CRC16
{
/**
* Calculates the 16-bit CRC checksum for the message using polynomial 0x1021 and residual 0xFFFF
* @param message with transmitted 16-bit checksum at the end.
* @return true if the check is correct or false if it fails the CRC check.
*/
public static boolean check(BinaryMessage message)
{
BinaryMessage copy = message.copy();
BinaryMessage polynomial = BinaryMessage.loadHex("11021");
polynomial.rotateLeft(3, 0, 20);
polynomial.setSize(message.size());
int previousX = 0;
for(int x = copy.nextSetBit(0); x >= 0 && x < copy.size() - 16; x = copy.nextSetBit(x + 1))
{
polynomial.rotateRight(x - previousX, previousX, 17 + x);
previousX = x;
copy.xor(polynomial);
}
IntField crc = IntField.length16(copy.size() - 16);
return copy.getInt(crc) == 0xFFFF;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -44,9 +44,9 @@ public class Checksum_5_DMR
/**
* Indicates if the 77-bit Full Link Control message is valid.
* @param message to check
* @return true if the checksum matches.
* @return residual value leftover from xor of the calculated and transmitted checksums.
*/
public static boolean isValid(CorrectedBinaryMessage message)
public static int isValid(CorrectedBinaryMessage message)
{
int accumulator = message.getInt(LC_0);
accumulator += message.getInt(LC_1);
@@ -57,10 +57,8 @@ public class Checksum_5_DMR
accumulator += message.getInt(LC_6);
accumulator += message.getInt(LC_7);
accumulator += message.getInt(LC_8);
accumulator = accumulator % 31;
int checksum = message.getInt(CHECKSUM);
return accumulator == checksum;
return accumulator ^ checksum;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by

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