Compare commits

..

92 Commits

Author SHA1 Message Date
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
Denny Sheirer
4977c3e4fa #2022 Audio output reset now reacquires gain and mute controls. (#2023)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-16 04:09:37 -04:00
Denny Sheirer
54a0ca5247 #2020 P25 channel null frequency band error. (#2021)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-16 02:59:55 -04:00
Denny Sheirer
024a624083 #2018 P25 Phase 1 resolves issue where NowPlaying identifiers were flickering as they are removed and readded with each TDULC terminator message. (#2019)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-15 09:53:52 -04:00
Denny Sheirer
9a02416403 #2016 Fixes code regression introduced in #1973 with null playback preferences. (#2017)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-14 17:29:58 -04:00
Denny Sheirer
91e5bc8ba1 #1973 JUnit test for patched talkgroup audio call streaming that tests streaming the audio as the patched group or as the individual patched talkgroups according to the user preferences setting in call management. (#2015)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-14 15:20:53 -04:00
Denny Sheirer
97afdfa8a9 #2013 Enhance SUID extension message handling and correct encryption sync when the algorithm is detected as ACCORDION and the key ID and MI are both zero, indicating a bad algorithm ID value. (#2014)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-14 13:32:40 -04:00
Denny Sheirer
67f91b80f2 #2006 AudioOutput now requests a new source data line from the mixer when it detects that the line no longer accepts audio byte data. (#2011)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-13 11:58:03 -04:00
Denny Sheirer
1b3ce431e6 #2007 P25 Call events tracking. P25 Traffic channel manager now uses event trackers to deconflict between control channel and traffic channel event updates. Protects against Motorola ISSI FQSUID that's all zeros. Resolves issue with running out of traffic channels after losing the control channel and going into CC hunt mode a few times. (#2010)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-13 11:53:12 -04:00
Denny Sheirer
d24895575c #2004 P25P1 Link Control Extended Source messages now process through decoder state correctly and display in Now Playing. (#2005)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-05 08:53:27 -04:00
Denny Sheirer
c97e355349 #2002 P25 fully qualified identifiers now formatted correctly in now playing window. (#2003)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-05 05:06:51 -04:00
Denny Sheirer
3c8cd0386d #1914 P25 P1 & P2 Talker Alias Support for Motorola and L3Harris. Talker alias manager caches aliases and applies to future events. Active (cached) talker aliases now listed in the channel Details tab. Enables multi-select in message viewer for all supported protocols. Note: decoding of Motorola's talker alias format not yet implemented - only recognition and reassembly of the encoded sequence is currently supported. (#2001)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-05 03:47:18 -04:00
Dennis Sheirer
600db3fb8a #1998 P25 Traffic Channel Manager error ConcurrentModification on Allocated Channels Map, introduced in #1996 2024-10-02 03:23:20 -04:00
Denny Sheirer
161595e7c8 #1996 P25P1 traffic channel monitor now clears allocated traffic channels each time the control frequency changes, which should be after a lose-sync event. This ensures that none of the traffic channel frequencies get blocked when the monitor thinks that a channel is already allocated. (#1997)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-10-01 04:03:25 -04:00
Denny Sheirer
0ecdf4dd2b #1994 P25 Phase1 missing subsequent calls audio when multiple conversations on the same traffic channel. (#1995)
Fixed issue where sdrtrunk was not checking HDU and LDU2 encryption sequences as valid before assessing the encryption state of a call.  Updated P25P1 audio module to check the valid flag for both the HDU and the LDU2 encryption segment before assigning encryption state.  Now caches all LDU messages until encryption state is determined.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-29 07:53:13 -04:00
Denny Sheirer
32ce79b8af #1990 Ignores GPS positions with 0/0 latitude and longitude values. (#1991)
(cherry picked from commit 55b20c75c0)

Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-09-22 09:56:00 -04:00
sheirerd
8070ae3c4b Revert "#1990 Ignores GPS positions with 0/0 latitude and longitude values."
This reverts commit 55b20c75c0.
2024-09-22 09:50:53 -04:00
sheirerd
55b20c75c0 #1990 Ignores GPS positions with 0/0 latitude and longitude values. 2024-09-22 09:46:11 -04:00
Denny Sheirer
5d4dff7948 #1985 Update to JDK 23 (#1987)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-20 09:33:36 -04:00
Denny Sheirer
b1229b6971 #1915 P25 audio playback can stop playing audio despite everything else appears to be normal. (#1986)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-19 15:23:31 -04:00
Denny Sheirer
9474d53a93 #1983 Reverts aliasing of fully qualified identifiers against locally assign ID or address values - must be an exact match to the fully qualified identifier. (#1984)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-18 03:51:47 -04:00
Denny Sheirer
6f05db723f #1981 Adds P25 Phase1 support for foreign system IDEN_UPD_TDMA messages on control channel. (#1982)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-17 03:54:16 -04:00
Doug Hoffman
056bccb89a Improvements to icecast failure handling (#1401)
- Add mechanism to persist failure reason while continuing to reconnect
- Add 'warning' attribute for failure conditions that may be temporary,
  but still need to be reported
- Add logging for failures
- Prevent possibly temporary server-side conditions from blocking
  reconnect attempts
- Detect icecast 2.4 auth failures
- Prevent generic errors from overriding existing error state
2024-09-15 08:48:00 -04:00
Denny Sheirer
e00cd90aef #1939 Duplicate call detector enhancements and unit testing. (#1980)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-15 08:39:53 -04:00
Denny Sheirer
8bb59d581f #1977 P25 FQSUID handling improvements. P25 fully qualified radio/talkgroup identifiers we're being flagged as invalid when ISSI patch assigned a local address/ID of zero. Changes format of FQSUID and FQTGID to use a value of zero for the local alias when the call type is an ISSI patch and suppress display of the 0 alias value. If the identifier represents a roaming user, then the local address value is displayed with the FQSUID appended in parenthesis. (#1978)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-15 05:37:01 -04:00
Denny Sheirer
b9068da99c #1971 P25 AMBTC Status Query - Error Parsing Source Address (#1976)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-09 09:37:09 -04:00
Denny Sheirer
9f3f7e65a4 #1974 P25 Fully Qualified Talkgroup/Radio values shouldn't replace local ID/address, when the value is zero, with the fully qualified ID, which causes incorrect aliasing. (#1975)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-09 09:24:05 -04:00
Denny Sheirer
e6a804836b #1948 Audio Stream starts are now spun off to a cached thread pool to prevent any initial stream connections from delaying the application startup. (#1968)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-03 04:13:25 -04:00
Denny Sheirer
5d8d3e4a41 #1966 P25 aliases not correctly matching when radio system is using fully qualified talkgroup/radio values. (#1967)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-02 08:05:17 -04:00
Denny Sheirer
298d9d7016 #1964 Mac OS issue when loading SDRPlay API native library from fully qualified path. (#1965)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-09-02 05:11:03 -04:00
Denny Sheirer
d7fbbcefc5 #1960 application root directory not found on fresh install (#1961)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-08-31 08:33:27 -04:00
Denny Sheirer
36e198eb1b #1955 Java JDK 22 and Gradle 8.10. Updates SDRPlay API to latest Project Panama FFM API changes. Increment app version to 0.6.1-beta2 (#1958)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-08-31 08:03:34 -04:00
Denny Sheirer
130e59f4c1 #1912 P25 Patch Group info no longer missing from Now Playing and recording and streaming metadata. (#1954)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-08-23 08:27:38 -04:00
Denny Sheirer
9223e7c056 #1952 P25 Traffic Channel Manager now clears stale call events from the traffic channel event map so that multiple calls don't get aggregated. (#1953)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-08-23 07:49:07 -04:00
Denny Sheirer
c3487411fb #1860 Resolves USB transfer buffer submit errors and resubmit error tracking. Synchronizes the submit transfers method so that multi-threaded access doesn't cause accounting errors. Ignores LIBUSB_ERROR_BUSY when (re)submitting due to accounting errors. Comments out several P25 unrecognized messaging loggers. (#1950)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-08-21 03:27:44 -04:00
Denny Sheirer
142dd9d579 1944 SDRPlay API version 3.16 (#1945)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-07-29 04:20:03 -04:00
Denny Sheirer
53d537bda8 #1781 Radio Reference API - adds logging to log results of test connection or user login attempt for troubleshooting RR enterprise user login issues. (#1943)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-07-29 04:14:33 -04:00
Denny Sheirer
24e11b8ed6 #1934 P25P1 conventional fallback message toString() output fixed. (#1935)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-06-02 07:15:20 -04:00
Denny Sheirer
14a7b040f9 #1931 Resolves issue where P25 Phase 2 L3Harris Patch Group message incorrectly includes Talkgroup/Radio identifiers incorrectly parsed from the subsequent message. Updated code to respect the self-reported message octet length and also to test for correct length before adding patch group member talkgroup and radio identifiers. (#1932)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-06-01 06:38:38 -04:00
Denny Sheirer
2af3e992a1 #1926 Toolkit not initialized error when running headless during APCO25 channel start processing request. (#1928)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-05-30 03:48:04 -04:00
Denny Sheirer
737c3faacc #1916 SDRPlay API 3.15 and RSPdxR2 support. (#1921)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-05-26 06:41:56 -04:00
Denny Sheirer
03792b0245 #1924 Resolves issue with talker GPS positions showing up in map panel as user identifiers. (#1925)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-05-26 06:29:07 -04:00
Denny Sheirer
dddda052fa #1849 Map enhancements. Adds track list, zoom to selection, history trail size and other features. (#1920)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-05-19 07:06:44 -04:00
Denny Sheirer
c50a914632 #1907 Hytera GPS positions, XPT Adjacent site, and Hytera Talker Aliases. (#1908)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-05-04 18:27:54 -04:00
Denny Sheirer
5f082973eb #190t P25 Phase 1 encrypted call status (#1906)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-05-04 13:14:07 -04:00
Denny Sheirer
2f0419f796 #1903 P25 Phase 2 audio - resolves 2-second chopped audio segments (#1904)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-05-04 06:58:45 -04:00
Denny Sheirer
cbe451a793 #1876 Unifies reentrant lock between tuners and frequency controller. (#1901)
Co-authored-by: sheirerd <sheirerd@ainfosec.com>
2024-05-04 05:29:54 -04:00
Denny Sheirer
aa8d449140 #1899 radio reference API version 18 and update site editor to use new 'tdma_cc' flag to distinguish P25 Phase 2 FDMA vs TDMA control channel sites. (#1900)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-04-27 10:32:46 -04:00
Denny Sheirer
7e709d9d6f #1304 P25 Phase 1/2 decoder enhancements. (#1898)
* P25 Phase 2 now supports control channel decoding and decoder was enhanced to improve decodes.
* Radio reference site editor updated for phase 2 sites to allow user to select FDMA or TDMA control.
* Decode events are now minimized and de-duplicated.
* Fully parsing all phase 1/2 messages from the latest ICD.
* Vendor specific messaging recoveries (Moto & Harris).
* Patch group management enhancements with stale patch detection.
* Tuner editor now fully resets the min/max frequency values
* Default traffic channel count increased to 20 (from 3).
* ISSI roaming radio and talkgroup values now displayed and aliasable as fully qualified values (e.g. 1234(123.456.7890)
* Moto emergency alarm activation messaging support
* Expanded list of P25 encryption algorithms
* Message (bits) viewer enhanced for P25 phase 1 and phase 2
* IPV4 addresses and UDP ports no longer classified as user values - won't display in event views.
* New framework for describing message binary fields that should reduce overall memory footprint and enhance code readability

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-04-26 13:55:17 -04:00
Denny Sheirer
f5938fa8e2 #1856 DMR traffic channel thread dead-lock. Removing synchronized method access since content access is already protected by the reentrant lock. The synchronized method is redundant and also causing the dead-lock. (#1857)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-13 04:05:35 -05:00
Denny Sheirer
ca2509e9f9 #1854 Tuner minimum frequency of 1 Hertz now supported. Resolves issue where the FrequencyTextField wouldn't accept minimum frequency values smaller than 1 MHz on initial setup. (#1855)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-12 04:21:56 -05:00
Denny Sheirer
1d2ba8c06c #1852 Spectral channel banners incorrect color for multi-frequency channels. (#1853)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-12 03:29:13 -05:00
Denny Sheirer
73275aa8ae #172 Allow users to adjust tuner min/max frequency ranges so that they can ensure tuners stay put and support channels in a predicatable manner. (#1850)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-11 17:08:43 -05:00
Denny Sheirer
2c32096ec9 #1847 Updates spectral display to show channel overlay for all frequencies in a channel configuration that contains multiple frequencies. (#1848)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-11 06:19:39 -05:00
Denny Sheirer
6532e33296 #1845 Updates polyphase tuner channel manager to avoid retuning for a new channel if all channels will continue to fit using the current center frequency (#1846)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-11 05:22:18 -05:00
Denny Sheirer
7ea8cb63d2 #1843 Map entity plotter concurrent modification exception (#1844)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-11 04:26:54 -05:00
Denny Sheirer
1df29b62eb #1841 DMR call events include channel and frequency (#1842)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-10 08:04:25 -05:00
Denny Sheirer
8a5b31f143 #1839 DMR tone identifiers don't clear at the end of a call. (#1840)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-10 07:39:37 -05:00
Denny Sheirer
4d7a8e5980 #1837 Channel Stream Thread Naming With Channel Config Number, Protocol and Frequency (#1838)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-10 07:30:33 -05:00
Denny Sheirer
aeeed71231 #1834 DMR CapMax Adv mode call events can have 'No Frequency'. Updated message class to ensure all 4x channels get nominated for timeslot frequency enrichment where previously the timeslot 2 channels might not receive a frequency enrichment. Also protected against bad logging for an UnknownCSBKMessage that gets constructed and CRC correction causing the message to have a valid channel grant opcode. (#1835)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-10 04:42:50 -05:00
Denny Sheirer
f6c4cc8a5f 1810 P25 Voice frame list concurrent modification error. Added synchronized to method to deconflict when the audio module or the binary frame recorder can access the message and not step on each other. Issue was noted in the phase 2 module, but update the phase 1 module as well to protect against the issue. (#1833)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-10 03:40:57 -05:00
Denny Sheirer
d019388034 1826 Vector Oscillator fails to generate MPT1327 correlator reference samples on SIMD-512 machines. (#1832)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 10:39:46 -05:00
Denny Sheirer
e125d6a452 #1830 Health & Diagnostic Monitor feature. (#1831)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 10:05:51 -05:00
Denny Sheirer
6b5a1fb827 #1802 Traffic channels stuck in TEARDOWN. Resolves: (#1829)
1. Tuner channel processing runnables were being interruptibly shutdown, preventing the channel from clearing any owned locks (ie DMRTrafficChannelManager) causing thread deadlock and eventually causing out of memory situation or tying up all threads in the thread pool to the point that the application stopped processing any data.

2. DMR Traffic Channel Manager was erroneously processing non-owned traffic channel events.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 06:05:19 -05:00
Denny Sheirer
0491b08818 #1827 USB Hotplug support callback can block the UI thread causing blank white screen. (#1828)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-09 03:38:12 -05:00
Denny Sheirer
7b9adc1da2 #1824 Adds diagnostic report generation to File>>Reports menu to log thread dumps and generate a processing diagnostic/status report. (#1825)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-04 09:59:53 -05:00
Denny Sheirer
3acaea0019 #1821 Resolves issue with SDRPlay IF Gain auto/manual modes and slider adjustment. (#1822)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-03 16:12:01 -05:00
Denny Sheirer
be02393f65 #1819 SDRPlay RSP1B tuner is now constructed correctly. (#1820)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-03 15:28:39 -05:00
Denny Sheirer
614bf72f01 SDRPlay API version 3.14 & RSP1B (#1817)
* #1812 sdrplay adds support for RSP1B and changes gain to expose LNA and Baseband gain settings for all tuners.

* Updates build to 0.6.1 beta 1.

---------

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-02-03 10:18:05 -05:00
Peter Hicks
bed28b82ab Fix the channel power meter so that the heading displays without truncation (#1814) 2024-01-30 04:10:49 -05:00
Peter Hicks
c047d48d91 Merge pull request #1813
* OSM Tileserver now supports TLSv1.3, so we should use this in prefere…
2024-01-30 04:08:46 -05:00
Moto
5222bffce4 Merge pull request #1808
* Inherit preferred tuner configuration to P25 traffic channels
2024-01-30 04:05:19 -05:00
1059 changed files with 76403 additions and 36355 deletions

View File

@@ -22,10 +22,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 20
- name: Set up JDK 23
uses: actions/setup-java@v3
with:
java-version: '20'
java-version: '23'
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 20
- name: Set up JDK 23
uses: actions/setup-java@v3
with:
java-version: '20'
java-version: '23'
distribution: 'liberica'
java-package: 'jdk+fx'

View File

@@ -29,6 +29,7 @@
<w>talkgroups</w>
<w>tgid</w>
<w>ultravox</w>
<w>unsynchronized</w>
<w>wacn</w>
<w>xtea</w>
</words>

2
.idea/misc.xml generated
View File

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

View File

@@ -1,36 +0,0 @@
Auto-generating Java code from SDRPlay API using jextract.
Note: the api headers in the 3.11 folder are equivalent to the 3.08 headers. The only
difference from the 3.07 headers is the addition of the boolean 'valid' flag to DeviceT structure.
1. Download JDK19 compatible build of jextract JDK: https://jdk.java.net/jextract/
2. If generating on windows platform, modify the sdrplay_api.h to remove the windows
pieces of the file. Specifically:
2a. Comment out these lines but leave the *HANDLE typedef:
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
//#include "windows.h"
//#elif defined (__GNUC__)
typedef void *HANDLE;
//#endif
//#ifndef _SDRPLAY_DLL_QUALIFIER
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
//#elif defined(STATIC_LIB) || defined(__GNUC__)
//#define _SDRPLAY_DLL_QUALIFIER
//#endif
//#endif // _SDRPLAY_DLL_QUALIFIER
2b. Remove the '_SDRPLAY_DLL_QUALIFIER' from in front of the method declarations
2c. Change the 3.11 version string to 3.08 either in the header file, or in the generated java class
3. Open command prompt to the root source code where the generated files should be placed:
..\SDRtrunk\sdrplay-api\src\main\java
4. Generate the code:
c:\Users\Denny\Downloads\jdk-19-ea2-panama\bin\jextract "C:\Users\Denny\git\sdrtrunk\sdrplay-api\api\v3_11\sdrplay_api.h" -l libsdrplay_api --source
5. Do this for all versions of the API.

View File

@@ -5,23 +5,23 @@
#include "sdrplay_api_rx_channel.h"
#include "sdrplay_api_callback.h"
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
//#include "windows.h"
//#elif defined (__GNUC__)
#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
#include "windows.h"
#elif defined (__GNUC__)
typedef void *HANDLE;
//#endif
#endif
//#ifndef _SDRPLAY_DLL_QUALIFIER
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
//#elif defined(STATIC_LIB) || defined(__GNUC__)
//#define _SDRPLAY_DLL_QUALIFIER
//#endif
//#endif // _SDRPLAY_DLL_QUALIFIER
#ifndef _SDRPLAY_DLL_QUALIFIER
#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
#elif defined(STATIC_LIB) || defined(__GNUC__)
#define _SDRPLAY_DLL_QUALIFIER
#endif
#endif // _SDRPLAY_DLL_QUALIFIER
// Application code should check that it is compiled against the same API version
// sdrplay_api_ApiVersion() returns the API version
#define SDRPLAY_API_VERSION (float)(3.11)
#define SDRPLAY_API_VERSION (float)(3.15)
// API Constants
#define SDRPLAY_MAX_DEVICES (16)
@@ -35,6 +35,8 @@ typedef void *HANDLE;
#define SDRPLAY_RSP2_ID (2)
#define SDRPLAY_RSPduo_ID (3)
#define SDRPLAY_RSPdx_ID (4)
#define SDRPLAY_RSP1B_ID (6)
#define SDRPLAY_RSPdxR2_ID (7)
// Enum types
typedef enum
@@ -125,6 +127,7 @@ typedef enum
sdrplay_api_Update_RspDx_RfNotchControl = 0x00000008,
sdrplay_api_Update_RspDx_RfDabNotchControl = 0x00000010,
sdrplay_api_Update_RspDx_HdrBw = 0x00000020,
sdrplay_api_Update_RspDuo_ResetSlaveFlags = 0x00000040,
// Reasons for master and slave mode
} sdrplay_api_ReasonForUpdateExtension1T;
@@ -178,6 +181,7 @@ typedef sdrplay_api_ErrT (*sdrplay_api_SelectDevice_t)(sdrplay_api_Device
typedef sdrplay_api_ErrT (*sdrplay_api_ReleaseDevice_t)(sdrplay_api_DeviceT *device);
typedef const char* (*sdrplay_api_GetErrorString_t)(sdrplay_api_ErrT err);
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastError_t)(sdrplay_api_DeviceT *device);
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastErrorByType_t)(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
typedef sdrplay_api_ErrT (*sdrplay_api_DisableHeartbeat_t)(void);
// Device API function types
@@ -199,27 +203,28 @@ extern "C"
#endif
// Comman API function definitions
sdrplay_api_ErrT sdrplay_api_Open(void);
sdrplay_api_ErrT sdrplay_api_Close(void);
sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Open(void);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Close(void);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
_SDRPLAY_DLL_QUALIFIER const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastErrorByType(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
// Device API function definitions
sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);

View File

@@ -29,12 +29,19 @@ typedef struct
} sdrplay_api_RspDuoParamsT;
typedef struct
{
unsigned char resetGainUpdate; // default: 0
unsigned char resetRfUpdate; // default: 0
} sdrplay_api_RspDuo_ResetSlaveFlagsT;
typedef struct
{
unsigned char biasTEnable; // default: 0
sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel; // default: sdrplay_api_RspDuo_AMPORT_2
unsigned char tuner1AmNotchEnable; // default: 0
unsigned char rfNotchEnable; // default: 0
unsigned char rfDabNotchEnable; // default: 0
sdrplay_api_RspDuo_ResetSlaveFlagsT resetSlaveFlags;
} sdrplay_api_RspDuoTunerParamsT;
#endif //SDRPLAY_API_RSPduo_H

View File

@@ -1,8 +1,6 @@
import java.text.SimpleDateFormat
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,12 +17,16 @@ import java.text.SimpleDateFormat
* ****************************************************************************
*/
import java.text.SimpleDateFormat
/**
* Instructions for building/compiling the sdrtrunk application.
*
* Prerequisites:
* Install and configure an OpenJDK version 20 that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
* - Optional: install and configure Gradle 8.2+
* Install and configure an OpenJDK version 22+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
* - Optional: install and configure Gradle 8.10+
*
* Scenario 1: run the application via gradle command line from the source code root directory:
* command: ./gradlew run
@@ -46,8 +48,6 @@ plugins {
id 'java'
id 'idea'
id 'org.beryx.runtime' version '1.12.7'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
}
repositories {
@@ -58,10 +58,10 @@ repositories {
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 20 is required for this version of the Project Panama preview/incubator feature
//Java 23 is required for this version of the Project Panama preview/incubator feature
java {
toolchain {
languageVersion = JavaLanguageVersion.of(20)
languageVersion = JavaLanguageVersion.of(23)
}
}
@@ -77,12 +77,11 @@ test {
dependencies {
// JUnit Tests
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.assertj:assertj-core:3.23.1'
testImplementation('org.junit.jupiter:junit-jupiter-api:5.9.0')
testImplementation 'org.mockito:mockito-core:4.8.0'
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.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
//Jitpack imports
implementation 'com.github.dnault:libresample4j:master'
@@ -90,53 +89,48 @@ dependencies {
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
//mavenCentral/jcenter imports
implementation 'ch.qos.logback:logback-classic:1.4.5'
implementation 'ch.qos.logback:logback-core:1.4.5'
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 'com.github.jiconfont:jiconfont-elusive:2.0.3'
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation 'com.github.wendykierp:JTransforms:3.1'
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.google.guava:guava:33.3.0-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:11.0'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.11.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
implementation 'org.apache.commons:commons-compress:1.21'
implementation 'org.apache.commons:commons-compress:1.26.0'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-csv:1.9.0'
implementation 'org.apache.derby:derby'
implementation 'org.apache.derby:derbytools'
implementation 'org.apache.mina:mina-core:2.2.1'
implementation 'org.apache.mina:mina-http:2.2.1'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'org.rauschig:jarchivelib:1.2.0'
implementation 'org.slf4j:slf4j-api:2.0.5'
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
def os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem
/**
* This is needed for the JDK19 panama and vector API ... until it moves out of incubation or preview
* This is needed for the JDK23 vector API ... until it moves out of incubation or preview
*/
tasks.withType(JavaCompile) {
options.compilerArgs.add("--enable-preview") //Panama Foreign Function/Memory is preview in JDK 19
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Needed while Panama Vector API remains in incubator
options.compilerArgs.add("--enable-preview") //Project Panama Vector API is still in incubator
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Project Panama Vector API is still in incubator
}
/**
@@ -145,10 +139,10 @@ tasks.withType(JavaCompile) {
def jvmArgsWindows =
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
'--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED', //windows users for JIDE
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
'--enable-native-access=ALL-UNNAMED',
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //Allows Project Panama Foreign Function access to system library
'--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
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //SDRPlay API library path
/**
* JVM Arguments for running the application on Linux.
@@ -157,8 +151,7 @@ def jvmArgsLinux =
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
'--enable-native-access=ALL-UNNAMED',
'-Djava.library.path=/usr/local/lib'] //Allows Project Panama Foreign Function access to system library
'--enable-native-access=ALL-UNNAMED'] //SDRPlay API library path
application {
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
@@ -199,28 +192,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-20.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-20.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-20.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-20.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-20.0.1-full'
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_windows_aarch64 = jdk_base + 'windows-arm64/jdk-22.0.2-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/20.0.1+10/bellsoft-jdk20.0.1+10-"
def jdk_download_base = "https://download.bell-sw.com/java/23.0.1+13/bellsoft-jdk23.0.1+13-"
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.
@@ -237,7 +233,7 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
//jdk.accessibility is used with assistive technologies like screen readers
//java.management for JVM resource monitoring
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management', 'java.instrument')
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
//Use auto-detected modules and 'add' any specified modules.
rt.extension.additive.set(true)
@@ -263,7 +259,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())
@@ -299,6 +300,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, [])
}

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.7.0-alpha-1
projectVersion=0.6.1-beta-3

Binary file not shown.

View File

@@ -1,6 +1,6 @@
#
# ******************************************************************************
# Copyright (C) 2014-2023 Dennis Sheirer
# Copyright (C) 2014-2024 Dennis Sheirer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -19,7 +19,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

18
gradlew vendored
View File

@@ -2,7 +2,7 @@
#
# ******************************************************************************
# 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
@@ -88,9 +88,6 @@ done
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -136,10 +133,13 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
@@ -147,7 +147,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=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -155,7 +155,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=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -200,6 +200,10 @@ if "$cygwin" || "$msys" ; then
done
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

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
@@ -663,11 +663,13 @@ public class AliasList
public Alias getAlias(TalkgroupIdentifier identifier)
{
//Attempt to do a fully qualified identifier match only
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
{
return mFullyQualifiedTalkgroupAliasMap.get(fqti.toString());
return mFullyQualifiedTalkgroupAliasMap.get(fqti.getFullyQualifiedTalkgroupAddress());
}
//Attempt to match the talkgroup value
int value = identifier.getValue();
Alias mapValue = mTalkgroupAliasMap.get(value);
@@ -676,6 +678,7 @@ public class AliasList
return mapValue;
}
//Alternatively, match the talkgroup to any talkgroup ranges
for(Map.Entry<TalkgroupRange, Alias> entry : mTalkgroupRangeAliasMap.entrySet())
{
if(entry.getKey().contains(value))
@@ -780,11 +783,13 @@ public class AliasList
public Alias getAlias(RadioIdentifier identifier)
{
//Match fully qualified identifier only.
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
{
return mFullyQualifiedRadioAliasMap.get(fqri.toString());
return mFullyQualifiedRadioAliasMap.get(fqri.getFullyQualifiedRadioAddress());
}
//Attempt to match against the radio identifier
int value = identifier.getValue();
Alias mapValue = mRadioAliasMap.get(value);
@@ -793,6 +798,7 @@ public class AliasList
return mapValue;
}
//Alternatively, attempt to match the radio address against any radio ranges.
for(Map.Entry<RadioRange, Alias> entry : mRadioRangeAliasMap.entrySet())
{
if(entry.getKey().contains(value))

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
@@ -33,14 +33,12 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Alias Model contains all aliases and is responsible for creation and management of alias lists. Alias lists are a
* set of aliases that all share a common alias list name and can be attached to a decoding channel for aliasing
* identifiers produced by channel decoder(s).
*/
@Component("aliasModel")
public class AliasModel
{
private final static Logger mLog = LoggerFactory.getLogger(AliasModel.class);
@@ -65,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-2024 Dennis Sheirer
* 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
@@ -24,19 +24,19 @@ import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.action.AliasActionType;
import io.github.dsheirer.alias.action.RecurringAction;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.gui.preference.audio.playback.ToneFrequency;
import io.github.dsheirer.gui.preference.audio.playback.ToneUtil;
import io.github.dsheirer.gui.preference.audio.playback.ToneVolume;
import io.github.dsheirer.gui.preference.playback.ToneFrequency;
import io.github.dsheirer.gui.preference.playback.ToneUtil;
import io.github.dsheirer.gui.preference.playback.ToneVolume;
import io.github.dsheirer.message.IMessage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
public class BeepAction extends RecurringAction
{

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-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
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
package io.github.dsheirer.audio;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
@@ -27,9 +27,9 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -37,6 +37,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -68,19 +69,20 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
private BooleanProperty mDuplicate = new SimpleBooleanProperty(false);
private BooleanProperty mEncrypted = new SimpleBooleanProperty(false);
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
private MutableIdentifierCollection mIdentifierCollection = new MutableIdentifierCollection();
private Broadcaster<IdentifierUpdateNotification> mIdentifierUpdateNotificationBroadcaster = new Broadcaster<>();
private List<float[]> mAudioBuffers = new CopyOnWriteArrayList();
private AtomicInteger mConsumerCount = new AtomicInteger();
private AliasList mAliasList;
private long mStartTimestamp = System.currentTimeMillis();
private long mSampleCount = 0;
private boolean mDisposing = false;
private AudioSegment mLinkedAudioSegment;
private int mTimeslot;
private List<String> mLeases = new ArrayList<>();
/**
* Constructs an instance
@@ -94,50 +96,6 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
mIdentifierCollection.setTimeslot(timeslot);
}
/**
* Adds a lease to this audio segment. This audio segment will remain alive until all leases are removed.
* @param lease to add
*/
public void addLease(String lease)
{
synchronized(mLeases)
{
if(mLeases.contains(lease))
{
mLog.warn("Attempt to obtain duplicate lease: " + lease);
}
else
{
mLeases.add(lease);
}
}
}
/**
* Removes the lease from this audio segment. Once all leases are removed, the audio segment is disposed.
* @param lease to remove
*/
public void removeLease(String lease)
{
synchronized(mLeases)
{
if(mLeases.contains(lease))
{
mLeases.remove(lease);
}
else
{
mLog.warn("Attempt to remove a non-existent lease: " + lease);
}
if(mLeases.isEmpty())
{
mLog.warn("Invoking dispose() on non-leased audio segment - last lease was: " + lease);
dispose();
}
}
}
/**
* Timeslot for this audio segment
*/
@@ -173,6 +131,25 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return (mSampleCount / 8); //8 kHz audio generates 8 samples per millisecond
}
/**
* Indicates if the audio segment contains encrypted audio.
*
* @return encrypted property
*/
public BooleanProperty encryptedProperty()
{
return mEncrypted;
}
/**
* Indicates if this audio segment is encrypted
* @return true if encrypted.
*/
public boolean isEncrypted()
{
return mEncrypted.get();
}
/**
* The complete property is used by the audio segment producer to signal that the segment is complete and no
* additional audio or identifiers will be added to the segment.
@@ -193,6 +170,28 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mComplete.get();
}
/**
* Duplicate call audio property. This flag is set to true whenever a duplicate call detection function detects
* an audio segment is a duplicate.
*/
public BooleanProperty duplicateProperty()
{
return mDuplicate;
}
/**
* An observable list of broadcast channels for this audio segment. Broadcast channels are added to this segment
* across the life-cycle of the segment. As each new alias identifier is added to the segment, any broadcast
* channels assigned to the alias are added to this list. The audio segment producer can also add broadcast
* channels to this list.
*
* @return observable set of broadcast channels
*/
public ObservableSet<BroadcastChannel> broadcastChannelsProperty()
{
return mBroadcastChannels;
}
/**
* Set of broadcast channels from identifier associated aliases for this segment.
*/
@@ -295,7 +294,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
* Adds the collection of identifiers to this segment's identifier collection
* @param identifiers to pre-load into this audio segment
*/
public void addIdentifiers(Collection<Identifier> identifiers)
void addIdentifiers(Collection<Identifier> identifiers)
{
for(Identifier identifier: identifiers)
{
@@ -323,6 +322,24 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
return mAudioBuffers.size();
}
/**
* Gets the audio buffer at the specified index
* @param index of the buffer to fetch
* @return audio buffer
* @throws IllegalArgumentException if requested index is not valid
*/
public float[] getAudioBuffer(int index)
{
if(0 <= index && index < getAudioBufferCount())
{
return mAudioBuffers.get(index);
}
else
{
throw new IllegalArgumentException("Requested audio buffer at index [" + index + "] does not exist");
}
}
/**
* Indicates if this audio segment has one or more audio buffers
*/
@@ -343,6 +360,34 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
mLinkedAudioSegment = null;
}
/**
* Increments the consumer count to indicate that a consumer is currently processing this segment. When the
* consumer count returns to zero, this indicates that all consumers are finished with the audio segment and the
* resources can be reclaimed.
*
* Consumer count should only be increased by the producer of the audio segment, or if a consumer distributes the
* segment to additional consumers.
*/
public void incrementConsumerCount()
{
mConsumerCount.incrementAndGet();
}
/**
* Decrements the consumer count. Consumers of this audio segment should invoke this method to signal that they
* will no longer need this audio segment. When all consumers are finished with an audio segment, the audio
* segment resources will be reclaimed.
*/
public void decrementConsumerCount()
{
int count = mConsumerCount.decrementAndGet();
if(count <= 0)
{
dispose();
}
}
/**
* Adds an audio buffer to this segment. Note the producer of the audio buffer should increment the user count
* of the buffer prior to adding it to this segment. This segment will decrement the audio buffer user count once
@@ -362,6 +407,11 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
throw new IllegalStateException("Can't add audio to an audio segment that is being disposed");
}
if(mAudioBuffers.isEmpty())
{
mStartTimestamp = System.currentTimeMillis() - 20;
}
mAudioBuffers.add(audioBuffer);
mSampleCount += audioBuffer.length;
}
@@ -391,6 +441,14 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
{
mIdentifierCollection.update(identifier);
/**
* If we have a late-add encryption key, set the encrypted flag to true.
*/
if(identifier instanceof EncryptionKeyIdentifier eki)
{
mEncrypted.set(eki.isEncrypted());
}
List<Alias> aliases = mAliasList.getAliases(identifier);
for(Alias alias: aliases)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.call;
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
@@ -41,8 +41,11 @@ public class AudioSegmentBroadcaster<T extends AudioSegment> extends Broadcaster
{
for(Listener<T> listener : getListeners())
{
audioSegment.addLease(listener.getClass().toString());
audioSegment.incrementConsumerCount();
listener.receive(audioSegment);
}
//Decrement consumer counter for this broadcaster
audioSegment.decrementConsumerCount();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,7 +19,6 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
@@ -29,11 +28,9 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.duplicate.CallManagementPreference;
import io.github.dsheirer.preference.duplicate.ICallManagementProvider;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -41,10 +38,10 @@ import java.util.Map;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
@@ -52,32 +49,44 @@ import org.springframework.stereotype.Component;
* Note: system in this context refers to the system name value that is used in channel configurations. All decoder
* channels must share the same system name for call duplication detection.
*/
@Component("duplicateCallDetector")
public class DuplicateCallDetector implements Listener<AudioSegment>
{
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
private CallManagementPreference mCallManagementPreference;
private ICallManagementProvider mCallManagementProvider;
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
@Resource
private UserPreferences mUserPreferences;
protected Listener<AudioSegment> mDuplicateCallDetectionListener;
/**
* Constructs an instance
* Constructs an instance.
* @param userPreferences to access the duplicate call detection preferences.
*/
public DuplicateCallDetector()
public DuplicateCallDetector(UserPreferences userPreferences)
{
this(userPreferences.getCallManagementPreference());
}
@PostConstruct
public void postConstruct()
/**
* Constructs an instance.
* @param callManagementProvider to provide call management preferences.
*/
public DuplicateCallDetector(ICallManagementProvider callManagementProvider)
{
mCallManagementPreference = mUserPreferences.getCallManagementPreference();
mCallManagementProvider = callManagementProvider;
}
/**
* Optional listener to be notified each time an audio segment is flagged as duplicate.
* @param listener to register
*/
public void setDuplicateCallDetectionListener(Listener<AudioSegment> listener)
{
mDuplicateCallDetectionListener = listener;
}
@Override
public void receive(AudioSegment audioSegment)
{
if(mCallManagementPreference.isDuplicateCallDetectionEnabled())
if(mCallManagementProvider.isDuplicateCallDetectionEnabled())
{
Identifier identifier = audioSegment.getIdentifierCollection()
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
@@ -92,57 +101,109 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
if(detector == null)
{
detector = new SystemDuplicateCallDetector();
detector = new SystemDuplicateCallDetector(mCallManagementProvider, system);
mDetectorMap.put(system, detector);
}
audioSegment.addLease(detector.getClass().toString());
detector.add(audioSegment);
audioSegment.removeLease(getClass().toString());
}
}
}
}
/**
* System level duplicate call detector. Uses a scheduled executor to run every 25 ms to compare all ongoing call
* audio segments to detect duplicates.
*
* All audio segments remain in the queue until they are flagged as complete. While in the queue, each call is
* compared against the others to detect duplicates. Once all calls are either flagged as complete or flagged as
* duplicate and removed, the queue is empty and the monitoring is shutdown until a new audio segment arrives and
* then the monitoring starts again.
*/
public class SystemDuplicateCallDetector
{
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private AtomicBoolean mMonitoring = new AtomicBoolean();
private final LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private final List<AudioSegment> mAudioSegments = new ArrayList<>();
private ScheduledFuture<?> mProcessorFuture;
private Lock mLock = new ReentrantLock();
private boolean mMonitoring = false;
private final ICallManagementProvider mCallManagementProvider;
private String mSystem;
public SystemDuplicateCallDetector()
/**
* Constructs an instance
* @param callManagementProvider to check for duplicate monitoring preferences
*/
public SystemDuplicateCallDetector(ICallManagementProvider callManagementProvider, String system)
{
mCallManagementProvider = callManagementProvider;
mSystem = system;
}
/**
* Adds the audio segment to the monitoring queue.
* @param audioSegment to add
*/
public void add(AudioSegment audioSegment)
{
//Block on audio segment queue so that we don't interfere with monitoring shutdown
synchronized(mAudioSegmentQueue)
mLock.lock();
try
{
mAudioSegmentQueue.add(audioSegment);
startMonitoring();
}
}
private void startMonitoring()
{
if(mMonitoring.compareAndSet(false, true))
finally
{
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> process(),
0, 25, TimeUnit.MILLISECONDS);
mLock.unlock();
}
}
/**
* Starts the call monitoring thread if it's not already running.
*
* Note: this method should only be called from a thread with the lock acquired.
*/
private void startMonitoring()
{
if(!mMonitoring)
{
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::process,
0, 25, TimeUnit.MILLISECONDS);
mMonitoring = true;
}
}
/**
* Stops the call monitoring thread if the audio segements queue is empty.
*
* Note: this should only be called from a separate thread and not from within the scheduled monitoring thread
* because cancelling the scheduled timer from within the process() method would kill the thread without
* releasing the lock, causing a deadlock.
*/
private void stopMonitoring()
{
if(mMonitoring.compareAndSet(true, false))
mLock.lock();
try
{
if(mProcessorFuture != null)
//Recheck the audio segments queue to make sure we didn't slip in another audio segment before we can
//shut down the scheduled monitoring thread.
if(mMonitoring && mAudioSegments.isEmpty())
{
mProcessorFuture.cancel(true);
if(mProcessorFuture != null)
{
mProcessorFuture.cancel(true);
mProcessorFuture = null;
}
mMonitoring = false;
}
}
finally
{
mLock.unlock();
}
}
/**
@@ -153,7 +214,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
*/
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
{
if(mCallManagementPreference.isDuplicateCallDetectionByTalkgroupEnabled())
if(mCallManagementProvider.isDuplicateCallDetectionByTalkgroupEnabled())
{
//Step 1 check for duplicate TO values
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
@@ -165,7 +226,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
}
}
if(mCallManagementPreference.isDuplicateCallDetectionByRadioEnabled())
if(mCallManagementProvider.isDuplicateCallDetectionByRadioEnabled())
{
//Step 2 check for duplicate FROM values
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
@@ -185,53 +246,51 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
* @param identifiers2
* @return
*/
private boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
public static boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
{
for(Identifier identifier1: identifiers1)
{
if(identifier1 instanceof TalkgroupIdentifier)
if(identifier1 instanceof TalkgroupIdentifier tgId1)
{
int talkgroup1 = ((TalkgroupIdentifier)identifier1).getValue();
int tg1 = tgId1.getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof TalkgroupIdentifier &&
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == tg1)
{
return true;
}
else if(identifier2 instanceof PatchGroupIdentifier &&
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
pgId2.getValue().getPatchGroup().getValue() == tg1)
{
return true;
}
}
}
else if(identifier1 instanceof PatchGroupIdentifier)
else if(identifier1 instanceof PatchGroupIdentifier pgId1)
{
int talkgroup1 = ((PatchGroupIdentifier)identifier1).getValue().getPatchGroup().getValue();
int talkgroup1 = pgId1.getValue().getPatchGroup().getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof TalkgroupIdentifier &&
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == talkgroup1)
{
return true;
}
else if(identifier2 instanceof PatchGroupIdentifier &&
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
pgId2.getValue().getPatchGroup().getValue() == talkgroup1)
{
return true;
}
}
}
else if(identifier1 instanceof RadioIdentifier)
else if(identifier1 instanceof RadioIdentifier raId1)
{
int radio1 = ((RadioIdentifier)identifier1).getValue();
int radio1 = raId1.getValue();
for(Identifier identifier2: identifiers2)
{
if(identifier2 instanceof RadioIdentifier && ((RadioIdentifier)identifier2).getValue() == radio1)
if(identifier2 instanceof RadioIdentifier raId2 && raId2.getValue() == radio1)
{
return true;
}
@@ -247,6 +306,8 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
*/
private void process()
{
mLock.lock();
try
{
//Transfer in newly arrived audio segments
@@ -258,12 +319,25 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
if(complete)
{
audioSegment.removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
}
return complete;
});
//Remove any encrypted audio segments.
mAudioSegments.removeIf(audioSegment -> {
boolean encrypted = audioSegment.isEncrypted();
if(encrypted)
{
audioSegment.decrementConsumerCount();
}
return encrypted;
});
//Only check for duplicates if there is more than one call
if(mAudioSegments.size() > 1)
{
@@ -274,7 +348,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
{
AudioSegment current = mAudioSegments.get(currentIndex);
if(!current.isDuplicate())
if(current.hasAudio() && !current.isDuplicate())
{
int checkIndex = currentIndex + 1;
@@ -287,8 +361,14 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
if(isDuplicate(current, toCheck))
{
toCheck.setDuplicate(true);
toCheck.removeLease(getClass().toString());
toCheck.decrementConsumerCount();
duplicates.add(toCheck);
//Notify optional listener that we flagged the call as duplicate.
if(mDuplicateCallDetectionListener != null)
{
mDuplicateCallDetectionListener.receive(toCheck);
}
}
}
@@ -302,26 +382,12 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
mAudioSegments.removeAll(duplicates);
}
//Finally, if the audio segment queue is empty, shutdown montitoring until a new segment arrives
//Finally, if the audio segment queue is now empty, shutdown monitoring until a new segment arrives.
//The monitor shutdown method has to be called on a separate thread so that we don't kill our current
// thread and fail to release the lock.
if(mAudioSegments.isEmpty())
{
//Block on the audio segment queue so that we can shutdown before any new segments are added, and
//allow the add(segment) to restart monitoring as soon as needed.
synchronized(mAudioSegmentQueue)
{
if(mAudioSegmentQueue.isEmpty())
{
try
{
stopMonitoring();
}
catch(Exception e)
{
mLog.error("Unexpected error during duplicate audio segment monitoring shutdown", e);
//Do nothing, we got interrupted
}
}
}
ThreadPool.CACHED.submit(this::stopMonitoring);
}
}
catch(Throwable t)
@@ -329,6 +395,10 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
mLog.error("Unknown error while processing audio segments for duplicate call detection. Please report " +
"this to the developer.", t);
}
finally
{
mLock.unlock();
}
}
}
}

View File

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

View File

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

View File

@@ -34,6 +34,7 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
private Listener<BroadcastEvent> mBroadcastEventListener;
private T mBroadcastConfiguration;
protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY);
protected ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
protected int mStreamedAudioCount = 0;
protected int mErrorAudioCount = 0;
protected int mAgedOffAudioCount = 0;
@@ -55,6 +56,14 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
return mBroadcastState;
}
/**
* Observable last bad broadcast state property
*/
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
{
return mLastBadBroadcastState;
}
/**
* Current state of the broadcastAudio connection
*/
@@ -68,10 +77,26 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
*/
public void setBroadcastState(BroadcastState broadcastState)
{
if(broadcastState == BroadcastState.CONNECTED)
{
mLastBadBroadcastState.setValue(null);
}
else if(broadcastState.isErrorState() || broadcastState.isWarningState())
{
mLastBadBroadcastState.setValue(broadcastState);
}
mBroadcastState.setValue(broadcastState);
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
}
/**
* Last bad state of the broadcastAudio connection
*/
public BroadcastState getLastBadBroadcastState()
{
return mLastBadBroadcastState.get();
}
/**
* Starts the broadcaster
*/

View File

@@ -211,9 +211,17 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
super.setBroadcastState(state);
if(mBroadcastState.get() != null && mBroadcastState.get().isErrorState())
if(mBroadcastState.get() != null)
{
stop();
if(mBroadcastState.get().isErrorState())
{
mLog.error("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
stop();
}
else if(mBroadcastState.get().isWarningState())
{
mLog.warn("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
}
}
if(!connected())

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
@@ -22,10 +22,7 @@ package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.audio.call.Call;
import io.github.dsheirer.audio.call.CompletedCall;
import io.github.dsheirer.audio.call.ICallEventListener;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
@@ -35,15 +32,14 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.record.AudioSegmentRecorder;
import io.github.dsheirer.record.RecordFormat;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.LinkedTransferQueue;
@@ -51,50 +47,59 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on
* disk and enqueues the temporary recording for streaming.
*/
@Component("audioStreamingManager")
public class AudioStreamingManager implements ICallEventListener
public class AudioStreamingManager implements Listener<AudioSegment>
{
private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingManager.class);
@Resource
private BroadcastModel mBroadcastModel;
@Resource
private LinkedTransferQueue<AudioSegment> mNewAudioSegments = new LinkedTransferQueue<>();
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private Listener<AudioRecording> mAudioRecordingListener;
private BroadcastFormat mBroadcastFormat;
private UserPreferences mUserPreferences;
private LinkedTransferQueue<CompletedCall> mCompletedCallQueue = new LinkedTransferQueue<>();
private List<CompletedCall> mCompletedCalls = new ArrayList<>();
private BroadcastFormat mBroadcastFormat = BroadcastFormat.MP3;
private ScheduledFuture<?> mAudioSegmentProcessorFuture;
private int mNextRecordingNumber = 1;
/**
* Constructs an instance
* @param listener to receive completed audio recordings
* @param broadcastFormat for temporary recordings
* @param userPreferences to manage recording directories
*/
public AudioStreamingManager()
public AudioStreamingManager(Listener<AudioRecording> listener, BroadcastFormat broadcastFormat, UserPreferences userPreferences)
{
mAudioRecordingListener = listener;
mBroadcastFormat = broadcastFormat;
mUserPreferences = userPreferences;
}
/**
* Primary receive method
*/
@Override
public void receive(AudioSegment audioSegment)
{
mNewAudioSegments.add(audioSegment);
}
/**
* Starts the scheduled audio segment processor
*/
@PostConstruct
public void start()
{
if(mAudioSegmentProcessorFuture == null)
{
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new CompletedCallProcessor(),
0, 250, TimeUnit.MILLISECONDS);
mAudioSegmentProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 250, TimeUnit.MILLISECONDS);
}
}
/**
* Stops the scheduled audio segment processor
*/
@PreDestroy
public void stop()
{
if(mAudioSegmentProcessorFuture != null)
@@ -103,53 +108,44 @@ public class AudioStreamingManager implements ICallEventListener
mAudioSegmentProcessorFuture = null;
}
for(CompletedCall completedCall: mCompletedCallQueue)
for(AudioSegment audioSegment: mNewAudioSegments)
{
completedCall.audioSegment().removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
}
mCompletedCallQueue.clear();
mNewAudioSegments.clear();
for(CompletedCall completedCall: mCompletedCalls)
for(AudioSegment audioSegment: mAudioSegments)
{
completedCall.audioSegment().removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
}
mCompletedCalls.clear();
mAudioSegments.clear();
}
/**
* Implements the ICallEventListener interface to receive and enqueue completed calls to process for streaming.
* @param call that was completed.
* @param audioSegment for the call
*/
@Override
public void completed(Call call, AudioSegment audioSegment)
{
mCompletedCallQueue.add(new CompletedCall(call, audioSegment));
}
@Override
public void added(Call call) {} //Not implemented
@Override
public void updated(Call call) {} //Not implemented
@Override
public void deleted(Call call) {} //Not implemented
/**
* Main processing method to process audio segments
*/
private void processCompletedCalls()
private void processAudioSegments()
{
CompletedCall completedCall = mCompletedCallQueue.poll();
mNewAudioSegments.drainTo(mAudioSegments);
while(completedCall != null)
Iterator<AudioSegment> it = mAudioSegments.iterator();
AudioSegment audioSegment;
while(it.hasNext())
{
AudioSegment audioSegment = completedCall.audioSegment();
audioSegment = it.next();
if(!(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled()))
if(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled())
{
if(mBroadcastModel != null && audioSegment.hasBroadcastChannels())
it.remove();
audioSegment.decrementConsumerCount();
}
else if(audioSegment.completeProperty().get())
{
it.remove();
if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
{
IdentifierCollection identifiers =
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
@@ -207,10 +203,9 @@ public class AudioStreamingManager implements ICallEventListener
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
}
}
}
audioSegment.removeLease(getClass().toString());
completedCall = mCompletedCallQueue.poll();
audioSegment.decrementConsumerCount();
}
}
}
@@ -240,7 +235,7 @@ public class AudioStreamingManager implements ICallEventListener
AudioRecording audioRecording = new AudioRecording(path, broadcastChannels, identifierCollection,
audioSegment.getStartTimestamp(), length);
mBroadcastModel.receive(audioRecording);
mAudioRecordingListener.receive(audioRecording);
}
catch(IOException ioe)
{
@@ -274,20 +269,20 @@ public class AudioStreamingManager implements ICallEventListener
}
/**
* Scheduled runnable to process completed calls.
* Scheduled runnable to process audio segments.
*/
public class CompletedCallProcessor implements Runnable
public class AudioSegmentProcessor implements Runnable
{
@Override
public void run()
{
try
{
processCompletedCalls();
processAudioSegments();
}
catch(Throwable t)
{
mLog.error("Error processing completed calls for streaming", t);
mLog.error("Error processing audio segments for streaming", t);
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -22,14 +22,16 @@ import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallBroadcaster;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzFeedConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcaster;
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2AudioStreamingBroadcaster;
@@ -63,26 +65,29 @@ public class BroadcastFactory
switch(configuration.getBroadcastServerType())
{
case BROADCASTIFY_CALL:
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration);
return new BroadcastifyCallBroadcaster((BroadcastifyCallConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case RDIOSCANNER_CALL:
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration);
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case OPENMHZ:
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration);
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
inputAudioFormat, mp3Setting, aliasModel);
case BROADCASTIFY:
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
inputAudioFormat, mp3Setting);
inputAudioFormat, mp3Setting, aliasModel);
case ICECAST_TCP:
return new IcecastTCPAudioBroadcaster((IcecastTCPConfiguration) configuration, inputAudioFormat,
mp3Setting);
mp3Setting, aliasModel);
case ICECAST_HTTP:
return new IcecastHTTPAudioBroadcaster((IcecastHTTPConfiguration) configuration, inputAudioFormat,
mp3Setting);
mp3Setting, aliasModel);
case SHOUTCAST_V1:
return new ShoutcastV1AudioBroadcaster((ShoutcastV1Configuration) configuration, inputAudioFormat,
mp3Setting);
mp3Setting, aliasModel);
case SHOUTCAST_V2:
return new ShoutcastV2AudioStreamingBroadcaster((ShoutcastV2Configuration) configuration,
inputAudioFormat, mp3Setting);
inputAudioFormat, mp3Setting, aliasModel);
case UNKNOWN:
default:
mLog.info("Unrecognized broadcastAudio configuration: " + configuration.getBroadcastFormat().name());

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -31,9 +31,9 @@ public enum BroadcastServerType
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
OPENMHZ("OpenMHz", "images/openmhz.png"),
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
OPENMHZ("OpenMHz", "images/openmhz.png"),
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
UNKNOWN("Unknown", null);

View File

@@ -23,90 +23,92 @@ public enum BroadcastState
/**
* General error in configuration that causes the remote server to reject the connection
*/
CONFIGURATION_ERROR("Configuration Error", true),
CONFIGURATION_ERROR("Configuration Error", true, false),
/**
* Connected to the broadcastAudio server and capable of streaming
*/
CONNECTED("Connected", false),
CONNECTED("Connected", false, false),
/**
* Connection interrupted, attempting to reconnect.
*/
CONNECTING("Connecting", false),
CONNECTING("Connecting", false, false),
/**
* Indicates the configuration is disabled
*/
DISABLED("Disabled", true),
DISABLED("Disabled", true, false),
/**
* Disconnected from the broadcastAudio server
*/
DISCONNECTED("Disconnected", false),
DISCONNECTED("Disconnected", false, false),
/**
* Invalid credentials - user name or password
*/
INVALID_CREDENTIALS("Invalid User Name/Password", true),
INVALID_CREDENTIALS("Invalid User Name/Password", true, false),
/**
* Invalid mount point
*/
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true, false),
/**
* Invalid configuration settings
*/
INVALID_SETTINGS("Invalid Settings", true),
INVALID_SETTINGS("Invalid Settings", true, false),
/**
* Remote server max sources has been exceeded
*/
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", false, true),
/**
* Specified mount point is already in use
*/
MOUNT_POINT_IN_USE("Mount Point In Use", false),
MOUNT_POINT_IN_USE("Mount Point In Use", false, true),
/**
* Network is unavailable or the server address cannot be resolved
*/
NETWORK_UNAVAILABLE("Network Unavailable", false),
NETWORK_UNAVAILABLE("Network Unavailable", false, false),
/**
* Server is not known or reachable
*/
NO_SERVER("No Server", false),
NO_SERVER("No Server", false, false),
/**
* Initial state with no connection attempted.
*/
READY("Ready", false),
READY("Ready", false, false),
/**
* Error while broadcasting stream data. Temporary error state to allow connection to be reset.
*/
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false),
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false, false),
/**
* Unsupported audio format
*/
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true),
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true, false),
/**
* Unspecified error
*/
ERROR("Error", true);
ERROR("Error", true, false);
private String mLabel;
private boolean mErrorState;
private boolean mWarningState;
BroadcastState(String label, boolean errorState)
BroadcastState(String label, boolean errorState, boolean warningState)
{
mLabel = label;
mErrorState = errorState;
mWarningState = warningState;
}
public String toString()
@@ -118,4 +120,9 @@ public enum BroadcastState
{
return mErrorState;
}
public boolean isWarningState()
{
return mWarningState;
}
}

View File

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

View File

@@ -37,6 +37,7 @@ public class ConfiguredBroadcast
private BroadcastConfiguration mBroadcastConfiguration;
private AbstractAudioBroadcaster mAudioBroadcaster;
private ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>();
private ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
/**
* Constructs an instance
@@ -89,6 +90,14 @@ public class ConfiguredBroadcast
return mBroadcastState;
}
/**
* Last bad broadcast state of the configured audio broadcaster (optional)
*/
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
{
return mLastBadBroadcastState;
}
/**
* Sets the audio broadcaster
* @param audioBroadcaster to use for this configuration
@@ -96,11 +105,13 @@ public class ConfiguredBroadcast
public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster)
{
mBroadcastState.unbind();
mLastBadBroadcastState.unbind();
mAudioBroadcaster = audioBroadcaster;
if(audioBroadcaster != null)
{
mBroadcastState.bind(mAudioBroadcaster.broadcastStateProperty());
mLastBadBroadcastState.bind(mAudioBroadcaster.lastBadBroadcastStateProperty());
}
else
{
@@ -120,6 +131,7 @@ public class ConfiguredBroadcast
{
mBroadcastState.setValue(BroadcastState.CONFIGURATION_ERROR);
}
mLastBadBroadcastState.setValue(null);
}
}

View File

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

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

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -18,6 +18,7 @@
*/
package io.github.dsheirer.audio.broadcast.icecast;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
@@ -27,11 +28,13 @@ import io.github.dsheirer.audio.convert.MP3Setting;
public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
{
private IBroadcastMetadataUpdater mMetadataUpdater;
protected AliasModel mAliasModel;
public IcecastAudioBroadcaster(BroadcastConfiguration broadcastConfiguration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting)
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(broadcastConfiguration, inputAudioFormat, mp3Setting);
mAliasModel = aliasModel;
}
/**
@@ -47,7 +50,7 @@ public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration());
mMetadataUpdater = new IcecastBroadcastMetadataUpdater(getConfiguration(), mAliasModel);
}
return mMetadataUpdater;

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -26,7 +26,6 @@ import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.Arrays;
@@ -55,8 +54,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private static final String HTTP_1_0_OK_HEX_DUMP = "48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B";
@Resource
private AliasModel mAliasModel;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private Map<String,String> mHTTPHeaders;
@@ -76,9 +74,9 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
* @param configuration for the Icecast stream
*/
public IcecastHTTPAudioBroadcaster(IcecastHTTPConfiguration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting)
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(configuration, inputAudioFormat, mp3Setting);
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
}
/**
@@ -343,15 +341,29 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
}
else
{
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
mLog.error("String [" + getStreamName() + "] - HTTP 403 protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
}
break;
case 401: //Unauthorized
if(message.toString().contains("Authentication Required"))
{
mLog.error("Stream [" + getStreamName() + "] - unable to connect - invalid credentials");
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
disconnect();
}
else
{
mLog.error("String [" + getStreamName() + "] - HTTP 401 protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
}
break;
default:
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
}
}
else
@@ -403,9 +415,16 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
disconnect();
break;
default:
setBroadcastState(BroadcastState.ERROR);
disconnect();
/**
* Only allow a generic error to update state if we've not already experienced a more
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
*/
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.ERROR);
}
mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
disconnect();
break;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -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;
@@ -27,7 +28,6 @@ import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
@@ -40,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;
@@ -52,12 +54,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds
private static final int WRITE_TIMEOUT_SECONDS = 5;
@Resource
private AliasModel mAliasModel;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private long mLastConnectionAttempt = 0;
private boolean mVerboseLogging = false;
private AtomicBoolean mConnecting = new AtomicBoolean();
/**
@@ -72,9 +74,14 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
* @param configuration for the Icecast stream
*/
public IcecastTCPAudioBroadcaster(IcecastTCPConfiguration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting)
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(configuration, inputAudioFormat, mp3Setting);
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
if(configuration instanceof BroadcastifyFeedConfiguration broadcastify)
{
mVerboseLogging = broadcastify.isVerboseLogging();
}
}
/**
@@ -134,9 +141,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()));
@@ -150,6 +160,10 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
public void run()
{
if(mVerboseLogging)
{
mLog.info("Attempting connection ...");
}
setBroadcastState(BroadcastState.CONNECTING);
try
@@ -158,19 +172,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;
@@ -178,12 +215,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
}
@@ -192,6 +239,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);
}
@@ -211,6 +263,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
@@ -219,6 +276,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);
}
@@ -284,12 +347,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
@@ -313,15 +387,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())
{
@@ -349,7 +431,15 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
else
{
mLog.error("Unrecognized server response:" + message);
setBroadcastState(BroadcastState.ERROR);
/**
* Only allow a generic error to update state if we've not already experienced a more
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
*/
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.ERROR);
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -19,43 +19,55 @@
package io.github.dsheirer.audio.broadcast.openmhz;
import com.google.common.net.HttpHeaders;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import 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.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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;
@@ -81,16 +93,18 @@ import java.util.concurrent.TimeUnit;
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
@Resource
private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
* @param config to use
* @param aliasModel for access to aliases
*/
public OpenMHzBroadcaster(OpenMHzConfiguration config)
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(config);
mAliasModel = aliasModel;
}
/**
@@ -577,7 +591,7 @@ import java.util.concurrent.TimeUnit;
return "No Response";
}
catch(Exception e)
{
{
Throwable throwableCause = e.getCause();
if(throwableCause instanceof ConnectException)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2020 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,18 +17,24 @@
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.openmhz;
package io.github.dsheirer.gui.playlist.streaming;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.gui.playlist.streaming.AbstractBroadcastEditor;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.controlsfx.control.ToggleSwitch;
/**
* OpenMHz calls API configuration editor
@@ -44,9 +50,11 @@ import org.slf4j.LoggerFactory;
/**
* Constructs an instance
* @param playlistManager for accessing the broadcast model
*/
public OpenMHzEditor()
public OpenMHzEditor(PlaylistManager playlistManager)
{
super(playlistManager);
}
@Override

View File

@@ -27,6 +27,8 @@ 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;
@@ -38,7 +40,6 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.Resource;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
@@ -77,16 +78,18 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
@Resource
private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
* @param config to use
* @param aliasModel for access to aliases
*/
public RdioScannerBroadcaster(RdioScannerConfiguration config)
public RdioScannerBroadcaster(RdioScannerConfiguration config, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(config);
mAliasModel = aliasModel;
}
/**
@@ -437,6 +440,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
*/
private String getTalkgroupLabel(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
@@ -461,6 +465,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
*/
private String getTalkgroupGroup(AudioRecording audioRecording)
{
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -18,6 +18,7 @@
*/
package io.github.dsheirer.audio.broadcast.shoutcast.v1;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AudioStreamingBroadcaster;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
@@ -25,10 +26,6 @@ import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.util.ThreadPool;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
@@ -40,6 +37,11 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1AudioBroadcaster.class);
@@ -48,6 +50,7 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
private IBroadcastMetadataUpdater mMetadataUpdater;
private AliasModel mAliasModel;
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
@@ -62,9 +65,10 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
* @param configuration for the Shoutcast stream
*/
public ShoutcastV1AudioBroadcaster(ShoutcastV1Configuration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting)
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(configuration, inputAudioFormat, mp3Setting);
mAliasModel = aliasModel;
}
/**
@@ -80,7 +84,7 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
{
if(mMetadataUpdater == null)
{
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getConfiguration());
mMetadataUpdater = new ShoutcastV1BroadcastMetadataUpdater(getConfiguration(), mAliasModel);
}
return mMetadataUpdater;

View File

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

View File

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

View File

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

View File

@@ -1,241 +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.audio.call;
import io.github.dsheirer.audio.playbackfx.AudioPlaybackChannelController;
import io.github.dsheirer.audio.playbackfx.IAudioPlaybackStatusListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Separator;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
/**
* Audio playback controller.
*/
public class AudioPlaybackControlView extends HBox implements IAudioPlaybackStatusListener
{
private static final int ICON_SIZE = 16;
private ObjectProperty<MediaPlayer> mMediaPlayer = new SimpleObjectProperty<>();
private ToggleButton mAutoRepeatButton;
private ToggleGroup mPlaybackGroup;
private ToggleButton mPlayButton;
private ToggleButton mPauseButton;
private ToggleButton mStopButton;
private boolean mUpdating = false;
/**
* Constructs an instance
*/
public AudioPlaybackControlView()
{
setSpacing(2);
Separator separator = new Separator(Orientation.VERTICAL);
separator.setPadding(new Insets(0, 5, 0 , 7));
getChildren().addAll(getStopButton(), getPlayButton(), getPauseButton(), getAutoRepeatButton());
mediaPlayerProperty().addListener((observable, oldValue, newValue) -> updateControls());
}
/**
* Implements the IAudioPLaybackStatusListener method to click the stop button when end of media is detected.
* @param controller where this occurred.
*/
@Override
public void endOfMedia(AudioPlaybackChannelController controller)
{
getStopButton().setSelected(true);
}
@Override
public void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus,
MediaPlayer.Status currentStatus)
{
if(currentStatus != null)
{
switch(currentStatus)
{
case UNKNOWN:
break;
case READY:
break;
case PAUSED:
mUpdating = true;
getPauseButton().setSelected(true);
mUpdating = false;
break;
case PLAYING:
mUpdating = true;
getPlayButton().setSelected(true);
mUpdating = false;
break;
case STOPPED:
mUpdating = true;
getStopButton().setSelected(true);
mUpdating = false;
if(getAutoRepeatButton().isSelected())
{
getPlayButton().setSelected(true);
}
break;
case STALLED:
case HALTED:
case DISPOSED:
System.out.println("Playback is " + currentStatus);
break;
}
}
}
/**
* Update the controls when the media player changes
*/
private void updateControls()
{
boolean disable = mediaPlayerProperty().get() == null;
getStopButton().setDisable(disable);
getPlayButton().setDisable(disable);
getPauseButton().setDisable(disable);
getAutoRepeatButton().setDisable(disable);
}
/**
* Media player property that can be bound to a media player controller.
*/
public ObjectProperty<MediaPlayer> mediaPlayerProperty()
{
return mMediaPlayer;
}
/**
* Playback toggle buttons group (play, stop, and pause)
*/
private ToggleGroup getPlaybackGroup()
{
if(mPlaybackGroup == null)
{
mPlaybackGroup = new ToggleGroup();
mPlaybackGroup.selectedToggleProperty().addListener((observable, oldValue, selectedToggle) -> {
MediaPlayer mediaPlayer = mediaPlayerProperty().get();
if(!mUpdating && mediaPlayer != null)
{
if(getPlayButton().equals(selectedToggle))
{
mediaPlayer.play();
}
else if(getPauseButton().equals(selectedToggle))
{
mediaPlayer.pause();
}
else if(getStopButton().equals(selectedToggle))
{
mediaPlayer.stop();
}
}
});
}
return mPlaybackGroup;
}
/**
* Play button
*/
private ToggleButton getPlayButton()
{
if(mPlayButton == null)
{
mPlayButton = new ToggleButton();
mPlayButton.setToggleGroup(getPlaybackGroup());
mPlayButton.setTooltip(new Tooltip("Play Audio"));
mPlayButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.PLAY);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mPlayButton.setGraphic(iconNode);
}
return mPlayButton;
}
/**
* Pause button
*/
private ToggleButton getPauseButton()
{
if(mPauseButton == null)
{
mPauseButton = new ToggleButton();
mPauseButton.setToggleGroup(getPlaybackGroup());
mPauseButton.setTooltip(new Tooltip("Pause Audio Playback"));
mPauseButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.PAUSE);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mPauseButton.setGraphic(iconNode);
}
return mPauseButton;
}
/**
* Stop button
*/
private ToggleButton getStopButton()
{
if(mStopButton == null)
{
mStopButton = new ToggleButton();
mStopButton.setToggleGroup(getPlaybackGroup());
mStopButton.setTooltip(new Tooltip("Stop Audio Playback"));
mStopButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.STOP);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mStopButton.setGraphic(iconNode);
}
return mStopButton;
}
private ToggleButton getAutoRepeatButton()
{
if(mAutoRepeatButton == null)
{
mAutoRepeatButton = new ToggleButton();
mAutoRepeatButton.setTooltip(new Tooltip("Auto-repeat audio playback"));
mAutoRepeatButton.setDisable(true);
IconNode iconNode = new IconNode(FontAwesome.REPEAT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mAutoRepeatButton.setGraphic(iconNode);
}
return mAutoRepeatButton;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,29 +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.audio.call;
/**
* Completed call record
* @param call associated with the audio segment.
* @param audioSegment associated with the call.
*/
public record CompletedCall(Call call, AudioSegment audioSegment)
{
}

View File

@@ -1,32 +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.audio.call;
/**
* Listener interface for receiving paging requests from user initiated actions.
*/
public interface IPageRequestListener
{
/**
* Indicates the user requests to display a page number
* @param page number to display.
*/
void showPage(int page);
}

View File

@@ -1,236 +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.audio.call;
import java.text.NumberFormat;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.javafx.IconNode;
/**
* Implements a set of paging controls.
*/
public class PagingController extends HBox
{
private static final int ICON_SIZE = 20;
private NumberFormat mNumberFormat = NumberFormat.getIntegerInstance();
private Button mFirstButton;
private Button mLastButton;
private Button mNextButton;
private Button mPreviousButton;
private Label mPageLabel;
private int mPageCount = 1;
private int mCurrentPage = 1;
private IPageRequestListener mPageRequestListener;
/**
* Constructs an instance
* @param listener to receive page requests
*/
public PagingController(IPageRequestListener listener)
{
mPageRequestListener = listener;
setAlignment(Pos.CENTER);
setSpacing(0);
getChildren().addAll(getFirstButton(), getPreviousButton(), getPageLabel(), getNextButton(), getLastButton());
update(1, 1);
}
/**
* Updates the current state of this paging controller.
* @param currentPage number to display.
* @param pageCount the total number of available pages.
*/
public void update(int currentPage, int pageCount)
{
mPageCount = pageCount;
mCurrentPage = currentPage;
getFirstButton().setDisable(mCurrentPage <= 1);
getFirstButton().setTooltip(mCurrentPage <= 1 ? null : new Tooltip("Move to page 1"));
getPreviousButton().setDisable(mCurrentPage <= 1);
getPreviousButton().setTooltip(mCurrentPage <= 1 ? null : new Tooltip("Move to page " + (mCurrentPage - 1)));
getNextButton().setDisable(mCurrentPage >= mPageCount);
getNextButton().setTooltip(mCurrentPage >= mPageCount ? null : new Tooltip("Move to page " + (mCurrentPage + 1)));
getLastButton().setDisable(mCurrentPage >= mPageCount);
getLastButton().setTooltip(mCurrentPage >= mPageCount ? null : new Tooltip("Move to page " + mPageCount));
getPageLabel().setText("Page " + mNumberFormat.format(mCurrentPage) + " of " + mNumberFormat.format(mPageCount));
}
/**
* Total pages available
* @return page count
*/
public int getPageCount()
{
return mPageCount;
}
/**
* Current page number
* @return current page.
*/
public int getCurrentPage()
{
return mCurrentPage;
}
/**
* Notifies the listener that the user requests the page number
* @param page number requested
*/
private void request(int page)
{
if(mPageRequestListener != null)
{
mPageRequestListener.showPage(page);
}
}
/**
* Next page
*/
private void next()
{
request(mCurrentPage + 1);
}
/**
* Previous page
*/
private void previous()
{
request(mCurrentPage - 1);
}
/**
* First page
*/
private void first()
{
request(1);
}
/**
* Last page
*/
private void last()
{
request(mPageCount);
}
/**
* Current page status label.
*/
private Label getPageLabel()
{
if(mPageLabel == null)
{
mPageLabel = new Label("Page 1 of 1");
mPageLabel.setTooltip(new Tooltip("Current Page and Total Page Count"));
mPageLabel.setAlignment(Pos.CENTER);
mPageLabel.setMaxWidth(Double.MAX_VALUE);
mPageLabel.setPadding(new Insets(0, 20, 0, 20));
}
return mPageLabel;
}
/**
* First page button.
*/
private Button getFirstButton()
{
if(mFirstButton == null)
{
mFirstButton = new Button();
mFirstButton.setTooltip(new Tooltip("Go To First Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_LEFT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mFirstButton.setGraphic(iconNode);
mFirstButton.setOnAction(event -> first());
}
return mFirstButton;
}
/**
* Last page button
*/
private Button getLastButton()
{
if(mLastButton == null)
{
mLastButton = new Button();
mLastButton.setTooltip(new Tooltip("Go To Last Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_DOUBLE_RIGHT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mLastButton.setGraphic(iconNode);
mLastButton.setOnAction(event -> last());
}
return mLastButton;
}
/**
* Next page button
*/
private Button getNextButton()
{
if(mNextButton == null)
{
mNextButton = new Button();
mNextButton.setTooltip(new Tooltip("Go To Next Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_RIGHT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mNextButton.setGraphic(iconNode);
mNextButton.setOnAction(event -> next());
}
return mNextButton;
}
/**
* Previous page button.
*/
private Button getPreviousButton()
{
if(mPreviousButton == null)
{
mPreviousButton = new Button();
mPreviousButton.setTooltip(new Tooltip("Go To Previous Page"));
IconNode iconNode = new IconNode(FontAwesome.ANGLE_LEFT);
iconNode.setIconSize(ICON_SIZE);
iconNode.setFill(Color.BLACK);
mPreviousButton.setGraphic(iconNode);
mPreviousButton.setOnAction(event -> previous());
}
return mPreviousButton;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,17 +19,16 @@
package io.github.dsheirer.audio.codec.mbe;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.audio.AbstractAudioModule;
import io.github.dsheirer.audio.squelch.ISquelchStateListener;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -42,40 +41,44 @@ 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, IPreferenceUpdateListener
ISquelchStateListener
{
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private IAudioCodec mAudioCodec;
private UserPreferences mUserPreferences;
private final UserPreferences mUserPreferences;
private static Class sLoadedJmbeAudioConverterClass;
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
{
super(aliasList, timeslot, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
mUserPreferences = userPreferences;
MyEventBus.getGlobalEventBus().register(this);
loadConverter();
}
@PostConstruct
public void init()
@Override
protected void closeAudioSegment()
{
mUserPreferences.addUpdateListener(this);
}
super.closeAudioSegment();
@PreDestroy
public void destroy()
{
mUserPreferences.removeUpdateListener(this);
//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();
mAudioCodec = null;
MyEventBus.getGlobalEventBus().unregister(this);
}
public IAudioCodec getAudioCodec()
@@ -102,6 +105,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
*
* @param preferenceType that was updated
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.JMBE_LIBRARY)

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,12 +18,14 @@
*/
package io.github.dsheirer.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Broadcaster;
@@ -54,9 +56,10 @@ import javax.sound.sampled.SourceDataLine;
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
* and broadcasts audio segment metadata to registered listeners (ie gui components).
*/
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>, IPreferenceUpdateListener
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
{
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
private int mBufferStartThreshold;
private int mBufferStopThreshold;
private Listener<IdentifierCollection> mIdentifierCollectionListener;
@@ -104,23 +107,13 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
mMixer = mixer;
mMixerChannel = mixerChannel;
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mixerChannel.name()));
mUserPreferences = userPreferences;
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
mAudioFormat = audioFormat;
mLineInfo = lineInfo;
mRequestedBufferSize = requestedBufferSize;
mUserPreferences = userPreferences;
start();
}
/**
* Starts and configures this audio output
*/
private void start()
{
mUserPreferences.addUpdateListener(this);
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mMixerChannel.name()));
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
try
{
@@ -128,7 +121,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(mOutput != null)
{
mOutput.open(mAudioFormat, mRequestedBufferSize);
mOutput.open(audioFormat, requestedBufferSize);
//Start threshold: buffer is full with 10% or less of capacity remaining
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
@@ -147,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 [" +
mMixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
try
@@ -158,13 +152,14 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
mMixer.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
//Run the queue processor task every 100 milliseconds or 10 times a second
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
0, 100, TimeUnit.MILLISECONDS);
}
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
@@ -175,10 +170,13 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
catch(LineUnavailableException e)
{
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
mMixer.getMixerInfo().getName() + "]");
mMixer.getMixerInfo().getName() + "]");
}
updateToneInsertionAudioClips();
//Register to receive preference update notifications so we can update the preference items
MyEventBus.getGlobalEventBus().register(this);
}
/**
@@ -233,6 +231,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
/**
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.PLAYBACK)
@@ -277,6 +276,12 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
private void playAudio(ByteBuffer buffer)
{
if(mOutput == null)
{
LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request");
return;
}
if(buffer != null)
{
int wrote = 0;
@@ -297,6 +302,105 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
mOutputLastTimestamp = System.currentTimeMillis();
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)
{
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)
{
LOGGING_SUPPRESSOR.error("no gain control", 2, "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)
{
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
"MASTER MUTE control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
LOGGING_SUPPRESSOR.info("reopen audio output success", 2,
"Closed and reopened audio output - success - mOutput is not null");
}
else
{
LOGGING_SUPPRESSOR.info("reopen audio output fail", 2,
"Closed and reopened audio output - fail - mOutput is null");
return;
}
}
catch(LineUnavailableException lue)
{
LOGGING_SUPPRESSOR.error("reopen fail for lua", 2, "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", 2,
"Failed to (re)open the source data line for audio output - mixer [" +
mMixer.getMixerInfo().getName() + "]");
return;
}
}
}
if(mOutput.isRunning() && wrote < buffer.array().length)
@@ -315,7 +419,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
{
if(mCurrentAudioSegment != null)
{
mCurrentAudioSegment.removeLease(getClass().toString());
mCurrentAudioSegment.decrementConsumerCount();
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
mCurrentAudioSegment = null;
broadcast(null);
@@ -338,7 +442,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
if(isThrowaway(audioSegment))
{
audioSegment.removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
audioSegment = mAudioSegmentQueue.poll();
}
else
@@ -451,6 +555,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
mCanProcessAudio = false;
if(mProcessorFuture != null)

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,21 +18,19 @@
*/
package io.github.dsheirer.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioException;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
@@ -45,22 +43,20 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Manages scheduling and playback of audio segments to the local users audio system.
*/
@Component("audioPlaybackManager")
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController, IPreferenceUpdateListener
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
{
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private ScheduledFuture<?> mProcessingTask;
@Resource
private UserPreferences mUserPreferences;
private MixerChannelConfiguration mMixerChannelConfiguration;
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
@@ -72,15 +68,13 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
/**
* Constructs an instance.
*
* @param userPreferences for audio playback preferences
*/
public AudioPlaybackManager()
public AudioPlaybackManager(UserPreferences userPreferences)
{
}
@PostConstruct
public void postConstruct()
{
mUserPreferences.addUpdateListener(this);
mUserPreferences = userPreferences;
MyEventBus.getGlobalEventBus().register(this);
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
@@ -93,8 +87,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
catch(AudioException ae)
{
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
}
}
else
@@ -103,13 +97,6 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
}
@PreDestroy
public void preDestroy()
{
mUserPreferences.removeUpdateListener(this);
dispose();
}
/**
* Receives audio segments from channel audio modules.
* @param audioSegment
@@ -135,7 +122,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
if(newSegment.isDuplicate() &&
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
{
newSegment.removeLease(getClass().toString());
newSegment.decrementConsumerCount();
}
else if(newSegment.hasAudio())
{
@@ -164,7 +151,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
{
it.remove();
audioSegment.removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
}
else if(audioSegment.hasAudio())
{
@@ -176,7 +163,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
//Rare situation: the audio segment completed but never had audio ... dispose it
it.remove();
audioSegment.removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
}
}
}
@@ -198,7 +185,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
}
else if(audioSegment.isLinked())
{
@@ -207,9 +194,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
if(audioOutput.isLinkedTo(audioSegment))
{
it.remove();
audioSegment.addLease(audioOutput.getClass().toString());
audioOutput.play(audioSegment);
audioSegment.removeLease(getClass().toString());
}
}
}
@@ -226,10 +211,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
if(audioOutput.isEmpty())
{
AudioSegment audioSegment1 = mAudioSegments.remove(0);
audioSegment1.addLease(audioOutput.getClass().toString());
audioOutput.play(audioSegment1);
audioSegment1.removeLease(getClass().toString());
audioOutput.play(mAudioSegments.remove(0));
if(mAudioSegments.isEmpty())
{
@@ -249,7 +231,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
{
it.remove();
audioSegment.removeLease(getClass().toString());
audioSegment.decrementConsumerCount();
}
}
}
@@ -257,6 +239,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
if(mProcessingTask != null)
{
mProcessingTask.cancel(true);
@@ -271,6 +254,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
* Receive user preference update notifications so that we can detect when the user changes the audio output
* device in the user preferences editor.
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.PLAYBACK)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -21,12 +21,12 @@ package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import javax.sound.sampled.Mixer;
/**
* Mono Audio output implementation
*/
@@ -37,7 +37,7 @@ public class MonoAudioOutput extends AudioOutput
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
{
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO,
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* 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
@@ -21,12 +21,12 @@ package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import javax.sound.sampled.Mixer;
/**
* Stereo audio output implementation.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,41 +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.audio.playbackfx;
import javafx.scene.media.MediaPlayer;
/**
* Listener interface for audio playback status events.
*/
public interface IAudioPlaybackStatusListener
{
/**
* Notify the listener that the status of the media player has changed.
* @param controller for the media player
* @param previousStatus of the media player.
* @param currentStatus of the media player.
*/
void playbackStatusUpdated(AudioPlaybackChannelController controller, MediaPlayer.Status previousStatus, MediaPlayer.Status currentStatus);
/**
* Notifies the listener that current playback encountered the end of the audio media.
*/
void endOfMedia(AudioPlaybackChannelController controller);
}

View File

@@ -1,44 +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.audio.playbackfx;
import java.util.EnumSet;
/**
* Playback modes for left/right audio channels.
*/
public enum PlaybackMode
{
AUTO,
REPLAY,
LOCKED,
MUTE;
private static final EnumSet<PlaybackMode> AUTO_PLAY_MODES = EnumSet.of(AUTO, LOCKED);
/**
* Indicates if the playback mode is an autoplay mode (AUTO or LOCKED).
* @return true if an autoplay mode.
*/
public boolean isAutoPlayable()
{
return AUTO_PLAY_MODES.contains(this);
}
}

View File

@@ -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
@@ -451,6 +451,203 @@ public class BinaryMessage extends BitSet
return value;
}
/**
* Returns the integer value of the message field described by the field argument.
* @param intField with start and end indices (inclusive).
* @return integer field value.
*/
public int getInt(IntField intField)
{
int value = 0;
for(int index = intField.start(); index <= intField.end(); index++)
{
value = Integer.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the integer value of the message field described by the field argument.
* @param fragmentedField with an array of message indices
* @return integer field value.
*/
public int getInt(FragmentedIntField fragmentedField)
{
int value = 0;
for(int index: fragmentedField.indices())
{
value = Integer.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param intField to inspect
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(IntField intField)
{
return nextSetBit(intField.start()) <= intField.end();
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param fragmentedField to inspect
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(FragmentedIntField fragmentedField)
{
for(int index: fragmentedField.indices())
{
if(get(index))
{
return true;
}
}
return false;
}
/**
* Returns the integer value of the message field described by the field argument where the message start index is
* offset within this binary message.
* @param intField with start and end indices (inclusive).
* @param offset to the start of the first message index.
* @return integer field value.
*/
public int getInt(IntField intField, int offset)
{
int value = 0;
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
{
value = Integer.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the integer value of the message field described by the field argument where the message start index is
* offset within this binary message.
* @param fragmentedField with an array of field indices
* @param offset to the start of the first message index.
* @return integer field value.
*/
public int getInt(FragmentedIntField fragmentedField, int offset)
{
int value = 0;
for(int index: fragmentedField.indices())
{
value = Integer.rotateLeft(value, 1);
if(get(index + offset))
{
value++;
}
}
return value;
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param intField to inspect
* @param offset to the first bit of the message.
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(IntField intField, int offset)
{
return nextSetBit(intField.start() + offset) <= (intField.end() + offset);
}
/**
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
* @param intField to inspect
* @param offset to the first bit of the message.
* @return true if any of the bits are set, indicating a non-zero value.
*/
public boolean hasInt(FragmentedIntField fragmentedField, int offset)
{
for(int index: fragmentedField.indices())
{
if(get(index + offset))
{
return true;
}
}
return false;
}
/**
* Returns the long value of the message field described by the field argument.
* @param intField with start and end indices (inclusive).
* @return integer field value.
*/
public long getLong(LongField intField)
{
long value = 0;
for(int index = intField.start(); index <= intField.end(); index++)
{
value = Long.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the long value of the message field described by the field argument where the message start index is
* offset within this binary message.
* @param intField with start and end indices (inclusive).
* @param offset to the start of the first message index.
* @return field value.
*/
public long getLong(LongField intField, int offset)
{
long value = 0;
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
{
value = Long.rotateLeft(value, 1);
if(get(index))
{
value++;
}
}
return value;
}
/**
* Returns the integer value represented by the bit array
*
@@ -742,6 +939,18 @@ public class BinaryMessage extends BitSet
return sb.toString();
}
/**
* Returns the integer field formatted as a hex value using zero prefixes to pad the hex character count to fully
* represent the size (width) of the field.
* @param field to parse as hex
* @return hex value.
*/
public String getHex(IntField field)
{
int width = Math.ceilDiv(field.width(), 4);
return String.format("%0" + width + "X", getInt(field));
}
/**
* Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less
* than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in
@@ -848,14 +1057,14 @@ public class BinaryMessage extends BitSet
if(get(start))
{
//Negative value - flip and add one
BinaryMessage fragment = getSubMessage(start, end);
BinaryMessage fragment = getSubMessage(start, end + 1);
fragment.flip(0, fragment.size());
return fragment.getInt(1, fragment.size()) + 1;
return -(fragment.getInt(0, fragment.size() - 1) + 1);
}
else
{
//Positive value - return the contents.
return getInt(start + 1, end);
return getInt(start, end);
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.bits;
@@ -84,9 +81,9 @@ public class CorrectedBinaryMessage extends BinaryMessage
/**
* Returns a new binary message containing the bits from (inclusive) to end (exclusive).
*
* @param start bit
* @param end bit
* @return message
* @param start bit inclusive
* @param end bit exclusive
* @return message with sub message bits
*/
public CorrectedBinaryMessage getSubMessage(int start, int end)
{

View File

@@ -17,33 +17,29 @@
* ****************************************************************************
*/
package io.github.dsheirer.preference.retention;
import java.time.Duration;
package io.github.dsheirer.bits;
/**
* Age units
* Defines a fragmented or non-contiguous bit field within a binary message.
* @param indices for the bits in the field.
*/
public enum AgeUnits
public record FragmentedIntField(int... indices)
{
DAYS,
WEEKS,
MONTHS,
YEARS;
public FragmentedIntField
{
if(indices.length > 32)
{
throw new IllegalArgumentException("Integer field indices size [" + indices.length + "] cannot exceed 32-bits for an integer");
}
}
/**
* Returns a duration value in milliseconds that represents the value in the current units.
* @param value to calculate
* @return milliseconds of duration representing the age unit and value.
* Utility constructor method.
* @param indices (inclusive)
* @return constructed fragmented integer field.
*/
public long getDuration(int value)
public static FragmentedIntField of(int... indices)
{
return switch(this)
{
case DAYS -> Duration.ofDays(value).toMillis();
case WEEKS -> Duration.ofDays(7).toMillis() * value;
case MONTHS -> Duration.ofDays(31).toMillis() * value;
case YEARS -> Duration.ofDays(365).toMillis() * value;
};
return new FragmentedIntField(indices);
}
}

View File

@@ -0,0 +1,131 @@
/*
* *****************************************************************************
* 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.bits;
/**
* Defines a contiguous bit field within a binary message.
* @param start bit index, inclusive.
* @param end bit index, inclusive.
*/
public record IntField(int start, int end)
{
public IntField
{
if(end - start > 32)
{
throw new IllegalArgumentException("Integer field length [" + (end - start) + "] cannot exceed 32-bits for an integer");
}
if(end < start)
{
throw new IllegalArgumentException("Integer field start index must be less than end index");
}
}
/**
* Width of the field in bit positions
* @return width
*/
public int width()
{
return end() - start() + 1;
}
/**
* Utility constructor method.
* @param start index (inclusive)
* @param end index (inclusive)
* @return constructed bit field.
*/
public static IntField range(int start, int end)
{
return new IntField(start, end);
}
/**
* Utility constructor method for a field with one four bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length4(int start)
{
return new IntField(start, (start + 3));
}
/**
* Utility constructor method for a field with one octet of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length8(int start)
{
return new IntField(start, (start + 7));
}
/**
* Utility constructor method for a field with 12 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length12(int start)
{
return new IntField(start, (start + 11));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length16(int start)
{
return new IntField(start, (start + 15));
}
/**
* Utility constructor method for a field with 20 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length20(int start)
{
return new IntField(start, (start + 19));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length24(int start)
{
return new IntField(start, (start + 23));
}
/**
* Utility constructor method for a field with three octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length32(int start)
{
return new IntField(start, (start + 31));
}
}

View File

@@ -0,0 +1,122 @@
/*
* *****************************************************************************
* 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.bits;
/**
* Defines a contiguous bit field within a binary message.
* @param start bit index, inclusive.
* @param end bit index, inclusive.
*/
public record LongField(int start, int end)
{
public LongField
{
if(end - start > 64)
{
throw new IllegalArgumentException("Long field length [" + (end - start) + "] cannot exceed 64-bits for a long");
}
if(end < start)
{
throw new IllegalArgumentException("Long field start index must be less than end index");
}
}
/**
* Utility constructor method.
* @param start index (inclusive)
* @param end index (inclusive)
* @return constructed bit field.
*/
public static LongField range(int start, int end)
{
return new LongField(start, end);
}
/**
* Utility constructor method for a field with one four bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length4(int start)
{
return new LongField(start, (start + 3));
}
/**
* Utility constructor method for a field with one octet of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length8(int start)
{
return new LongField(start, (start + 7));
}
/**
* Utility constructor method for a field with 12 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length12(int start)
{
return new LongField(start, (start + 11));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length16(int start)
{
return new LongField(start, (start + 15));
}
/**
* Utility constructor method for a field with 20 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length20(int start)
{
return new LongField(start, (start + 19));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length24(int start)
{
return new LongField(start, (start + 23));
}
/**
* Utility constructor method for a field with three octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length32(int start)
{
return new LongField(start, (start + 31));
}
}

View File

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

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-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
@@ -22,6 +22,7 @@ package io.github.dsheirer.channel.metadata;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierUpdateListener;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
@@ -57,6 +58,7 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
private DecoderTypeConfigurationIdentifier mDecoderTypeConfigurationIdentifier;
private Identifier mFromIdentifier;
private List<Alias> mFromIdentifierAliases;
private Identifier mTalkerAliasIdentifier;
private Identifier mToIdentifier;
private List<Alias> mToIdentifierAliases;
private Integer mTimeslot;
@@ -67,7 +69,7 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
/**
* Constructs an instance
* @param aliasModel for alias lookup
* @param aliasModel for alias lookups
* @param timeslot for this metadata
*/
public ChannelMetadata(AliasModel aliasModel, Integer timeslot)
@@ -77,14 +79,38 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
}
/**
* Constructs an instance with timeslot (default = 0)
* @param aliasModel for alias lookup
* Constructs an instance
* @param aliasModel for alias lookups
*/
public ChannelMetadata(AliasModel aliasModel)
{
mAliasModel = aliasModel;
this(aliasModel, null);
}
/**
* Creates a textual description of this channel metadata.
*/
public String getDescription()
{
StringBuilder sb = new StringBuilder();
sb.append("Channel Metadata Description\n");
sb.append("\tTimeslot: ").append(mTimeslot).append("\n");
Identifier decoder = getDecoderTypeConfigurationIdentifier();
sb.append("\tDecoder: ").append(decoder != null ? decoder : "(null)").append("\n");
Identifier state = getChannelStateIdentifier();
sb.append("\tState: ").append(state != null ? state : "(null)").append("\n");
Identifier system = getSystemConfigurationIdentifier();
sb.append("\tSystem: ").append(system != null ? system : "(null)").append("\n");
Identifier site = getSiteConfigurationIdentifier();
sb.append("\tSite: ").append(site != null ? site : "(null)").append("\n");
Identifier channel = getChannelNameConfigurationIdentifier();
sb.append("\tChannel: ").append(channel != null ? channel : "(null)").append("\n");
Identifier frequency = getFrequencyConfigurationIdentifier();
sb.append("\tFrequency: ").append(frequency != null ? frequency : "(null)").append("\n");
Identifier logical = getDecoderLogicalChannelNameIdentifier();
sb.append("\tLogical Channel: ").append(logical != null ? logical : "(null)").append("\n");
return sb.toString();
}
public Integer getTimeslot()
{
@@ -214,6 +240,22 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
return mFromIdentifierAliases;
}
/**
* Optional talker alias identifier
*/
public Identifier getTalkerAliasIdentifier()
{
return mTalkerAliasIdentifier;
}
/**
* Indicates if we have a non-null talker alias identifier
*/
public boolean hasTalkerAliasIdentifier()
{
return mTalkerAliasIdentifier != null;
}
/**
* Current call event TO identifier
*/
@@ -385,15 +427,22 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
case USER:
if(identifier.getRole() == Role.FROM)
{
mFromIdentifier = update.isAdd() ? identifier : null;
if(mAliasList != null && mFromIdentifier != null)
if(identifier.getForm() == Form.TALKER_ALIAS)
{
mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier);
mTalkerAliasIdentifier = identifier;
}
else
{
mFromIdentifierAliases = Collections.EMPTY_LIST;
mFromIdentifier = update.isAdd() ? identifier : null;
if(mAliasList != null && mFromIdentifier != null)
{
mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier);
}
else
{
mFromIdentifierAliases = Collections.EMPTY_LIST;
}
}
broadcastUpdate(ChannelMetadataField.USER_FROM);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2024 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,17 +18,14 @@
*/
package io.github.dsheirer.channel.metadata;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
import io.github.dsheirer.preference.IPreferenceUpdateListener;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.annotation.Resource;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.Collection;
@@ -37,12 +34,10 @@ import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.swing.table.AbstractTableModel;
@Component("channelMetadataModel")
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener, IPreferenceUpdateListener
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataModel.class);
@@ -61,30 +56,17 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
private List<ChannelMetadata> mChannelMetadata = new ArrayList();
private Map<ChannelMetadata,Channel> mMetadataChannelMap = new HashMap();
private Listener<ChannelAndMetadata> mChannelAddListener;
@Resource
private UserPreferences mUserPreferences;
public ChannelMetadataModel()
{
}
@PostConstruct
public void postConstruct()
{
mUserPreferences.addUpdateListener(this);
}
@PreDestroy
public void preDestroy()
{
mUserPreferences.removeUpdateListener(this);
MyEventBus.getGlobalEventBus().register(this);
}
/**
* Receives preference update notifications via the event bus
* @param preferenceType that was updated
*/
@Override
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.TALKGROUP_FORMAT)
@@ -99,6 +81,52 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
}
}
/**
* Creates a diagnostic description of the channel metadata and metadata-channel map.
* @return diagnostic description.
*/
public String getDiagnosticInformation()
{
StringBuilder sb = new StringBuilder();
Map<ChannelMetadata,Channel> mapCopy = new HashMap<>(mMetadataChannelMap);
List<ChannelMetadata> metadataCopy = new ArrayList<>(mChannelMetadata);
sb.append("Channel Metadata Model Diagnostics\n");
for(ChannelMetadata channelMetadata: metadataCopy)
{
sb.append("\n--------------- CHANNEL METADATA ITEM --------------------\n\n");
if(mapCopy.containsKey(channelMetadata))
{
Channel channel = mapCopy.get(channelMetadata);
sb.append("\tMapped Channel:").append(channel).append("\n");
sb.append("\t\tSource Configuration: ").append(channel.getSourceConfiguration()).append("\n");
}
else
{
sb.append("\tMapped Channel: **Channel Metadata Is Not In the Metadata:Channel Map**\n");
}
sb.append(channelMetadata.getDescription());
}
//Check for orphaned map entries that don't match the current channel metadata list.
for(Map.Entry<ChannelMetadata,Channel> entry: mapCopy.entrySet())
{
if(!metadataCopy.contains(entry.getKey()))
{
sb.append("\n\tWARNING: orphaned Metadata Channel map entry - channel: " + entry.getValue()).append("\n");
}
}
return sb.toString();
}
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
}
/**
* Lookup model row for the specified channel metadata
*/

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
@@ -23,6 +23,7 @@ import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.channel.state.State;
import io.github.dsheirer.controller.channel.Channel;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.playlist.channel.ViewChannelRequest;
@@ -31,13 +32,12 @@ import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.MouseAdapter;
@@ -66,13 +66,9 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class);
private static final String TABLE_PREFERENCE_KEY = "channel.metadata.panel";
@Resource
private ChannelModel mChannelModel;
private ChannelProcessingManager mChannelProcessingManager;
@Resource
private IconModel mIconModel;
@Resource
private ChannelMetadataModel mChannelMetadataModel;
@Resource
private UserPreferences mUserPreferences;
private JTable mTable;
private Broadcaster<ProcessingChain> mSelectedProcessingChainBroadcaster = new Broadcaster<>();
@@ -84,20 +80,25 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
/**
* Table view for currently decoding channel metadata
*/
public ChannelMetadataPanel()
public ChannelMetadataPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences)
{
mChannelModel = playlistManager.getChannelModel();
mChannelProcessingManager = playlistManager.getChannelProcessingManager();
mIconModel = iconModel;
mUserPreferences = userPreferences;
init();
}
/**
* Initializes the panel
*/
@PostConstruct
private void postConstruct()
private void init()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
mTable = new JTable(mChannelMetadataModel);
mChannelMetadataModel.setChannelAddListener(new ChannelAddListener());
mTable = new JTable(mChannelProcessingManager.getChannelMetadataModel());
mChannelProcessingManager.getChannelMetadataModel().setChannelAddListener(new ChannelAddListener());
DefaultTableCellRenderer renderer = (DefaultTableCellRenderer)mTable.getDefaultRenderer(String.class);
renderer.setHorizontalAlignment(SwingConstants.CENTER);
@@ -118,7 +119,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
.setCellRenderer(new FrequencyCellRenderer());
//Add a table column width monitor to store/restore column widths
mTableColumnMonitor = new JTableColumnWidthMonitor(mTable, TABLE_PREFERENCE_KEY);
mTableColumnMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
JScrollPane scrollPane = new JScrollPane(mTable, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
@@ -166,11 +167,14 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
{
int selectedModelRow = mTable.convertRowIndexToModel(selectedViewRow);
ChannelMetadata selectedMetadata = mChannelMetadataModel.getChannelMetadata(selectedModelRow);
ChannelMetadata selectedMetadata = mChannelProcessingManager.getChannelMetadataModel()
.getChannelMetadata(selectedModelRow);
if(selectedMetadata != null)
{
mUserSelectedChannel = mChannelMetadataModel.getChannelFromMetadata(selectedMetadata);
mUserSelectedChannel = mChannelProcessingManager.getChannelMetadataModel()
.getChannelFromMetadata(selectedMetadata);
processingChain = mChannelProcessingManager.getProcessingChain(mUserSelectedChannel);
}
}
@@ -290,6 +294,10 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
{
text = EMPTY_VALUE;
}
else if(hasAdditionalIdentifier(channelMetadata))
{
text = text + " " + getAdditionalIdentifier(channelMetadata);
}
label.setText(text);
}
@@ -302,6 +310,8 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
}
public abstract Identifier getIdentifier(ChannelMetadata channelMetadata);
public abstract boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata);
public abstract Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata);
}
/**
@@ -319,6 +329,18 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
{
return channelMetadata.getFromIdentifier();
}
@Override
public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata)
{
return channelMetadata.getTalkerAliasIdentifier();
}
@Override
public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata)
{
return channelMetadata.hasTalkerAliasIdentifier();
}
}
/**
@@ -336,6 +358,11 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
{
return channelMetadata.getToIdentifier();
}
@Override
public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata) {return null;}
@Override
public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata) {return false;}
}
public class ColoredStateCellRenderer extends DefaultTableCellRenderer
@@ -400,11 +427,12 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
if(modelRowIndex >= 0)
{
ChannelMetadata metadata = mChannelMetadataModel.getChannelMetadata(modelRowIndex);
ChannelMetadata metadata = mChannelProcessingManager.getChannelMetadataModel().getChannelMetadata(modelRowIndex);
if(metadata != null)
{
Channel channel = mChannelMetadataModel.getChannelFromMetadata(metadata);
Channel channel = mChannelProcessingManager.getChannelMetadataModel()
.getChannelFromMetadata(metadata);
if(channel != null)
{
@@ -450,7 +478,7 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
if(metadata.size() > 0)
{
int modelRow = mChannelMetadataModel.getRow(metadata.get(0));
int modelRow = mChannelProcessingManager.getChannelMetadataModel().getRow(metadata.get(0));
if(modelRow >= 0)
{

View File

@@ -22,10 +22,12 @@ import com.jidesoft.swing.JideSplitPane;
import com.jidesoft.swing.JideTabbedPane;
import io.github.dsheirer.channel.details.ChannelDetailPanel;
import io.github.dsheirer.gui.power.ChannelPowerPanel;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.module.decode.event.DecodeEventPanel;
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.settings.SettingsManager;
import java.awt.Color;
import net.miginfocom.swing.MigLayout;
@@ -36,43 +38,30 @@ import javax.swing.JPanel;
*/
public class NowPlayingPanel extends JPanel
{
@Resource
private ChannelDetailPanel mChannelDetailPanel;
@Resource
private ChannelMetadataPanel mChannelMetadataPanel;
@Resource
private ChannelPowerPanel mChannelPowerPanel;
@Resource
private DecodeEventPanel mDecodeEventPanel;
private final MessageActivityPanel mMessageActivityPanel = new MessageActivityPanel();
private final ChannelMetadataPanel mChannelMetadataPanel;
private final ChannelDetailPanel mChannelDetailPanel;
private final DecodeEventPanel mDecodeEventPanel;
private final MessageActivityPanel mMessageActivityPanel;
private final ChannelPowerPanel mChannelPowerPanel;
private JideTabbedPane mTabbedPane;
private JideSplitPane mSplitPane;
private boolean mDetailTabsVisible = true;
private boolean mDetailTabsVisible;
/**
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
* messages, events, and spectral view.
*/
public NowPlayingPanel()
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
SettingsManager settingsManager, boolean detailTabsVisible)
{
}
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
mDetailTabsVisible = detailTabsVisible;
@PostConstruct
public void postConstruct()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
getSplitPane().add(mChannelMetadataPanel);
if(mDetailTabsVisible)
{
getSplitPane().add(getTabbedPane());
}
add(getSplitPane());
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
init();
}
/**
@@ -131,4 +120,21 @@ public class NowPlayingPanel extends JPanel
return mSplitPane;
}
private void init()
{
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
getSplitPane().add(mChannelMetadataPanel);
if(mDetailTabsVisible)
{
getSplitPane().add(getTabbedPane());
}
add(getSplitPane());
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelPowerPanel);
}
}

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,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.channel.state;
@@ -29,6 +26,7 @@ import io.github.dsheirer.message.IMessageListener;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.event.ActivitySummaryProvider;
import io.github.dsheirer.module.decode.event.DecodeEventDuplicateDetector;
import io.github.dsheirer.module.decode.event.IDecodeEvent;
import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
import io.github.dsheirer.sample.Broadcaster;
@@ -47,9 +45,38 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
protected Broadcaster<IDecodeEvent> mDecodeEventBroadcaster = new Broadcaster<>();
protected Listener<DecoderStateEvent> mDecoderStateListener;
private DecoderStateEventListener mDecoderStateEventListener = new DecoderStateEventListener();
private static final DecodeEventDuplicateDetector mDuplicateEventDetector = new DecodeEventDuplicateDetector();
private boolean mRunning;
public abstract DecoderType getDecoderType();
/**
* Implements module start and sets the mRunning flag to true so that messages can be processed.
*/
@Override
public void start()
{
mRunning = true;
}
/**
* Implements the module stop and sets the mRunning flag to false to stop message processing
*/
@Override
public void stop()
{
mRunning = false;
}
/**
* Indicates if this module is running and can process/pass messages down to sub-class implementations.
* @return true if running
*/
public boolean isRunning()
{
return mRunning;
}
/**
* Provides subclass reference to the decode event broadcaster
*/
@@ -80,7 +107,10 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
*/
protected void broadcast(IDecodeEvent event)
{
mDecodeEventBroadcaster.broadcast(event);
if(!mDuplicateEventDetector.isDuplicate(event, System.currentTimeMillis()))
{
mDecodeEventBroadcaster.broadcast(event);
}
}
/**
@@ -136,6 +166,9 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
mDecoderStateListener = null;
}
/**
* Wrapper that implements the IDecoderStateEventListener interface
*/
private class DecoderStateEventListener implements Listener<DecoderStateEvent>
{
@Override
@@ -144,5 +177,4 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
receiveDecoderStateEvent(event);
}
}
}

View File

@@ -1,102 +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.channel.state;
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.DecoderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Basic decoder channel state - provides the minimum channel state functionality
* to support an always un-squelched audio decoder.
*/
public class AlwaysUnsquelchedDecoderState extends DecoderState
{
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
private static final String NO_SQUELCH = "No Squelch";
private Identifier mChannelNameIdentifier;
private DecoderType mDecoderType;
private String mChannelName;
public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName)
{
mDecoderType = decoderType;
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : decoderType.name() + " CHANNEL";
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.USER, Form.CHANNEL_NAME, Role.TO);
}
@Override
public void init()
{
}
@Override
public String getActivitySummary()
{
StringBuilder sb = new StringBuilder();
sb.append("Activity Summary\n");
sb.append("\tDecoder:\t");
sb.append(mDecoderType);
sb.append("\n\n");
return sb.toString();
}
@Override
public void receive(IMessage t)
{
/* Not implemented */
}
@Override
public void receiveDecoderStateEvent(DecoderStateEvent event)
{
if(event.getEvent() == Event.REQUEST_RESET)
{
getIdentifierCollection().update(mChannelNameIdentifier);
}
}
@Override
public DecoderType getDecoderType()
{
return mDecoderType;
}
@Override
public void start()
{
broadcast(new DecoderStateEvent(this, Event.REQUEST_ALWAYS_UNSQUELCH, State.IDLE));
getIdentifierCollection().update(mChannelNameIdentifier);
}
@Override
public void stop()
{
}
}

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
@@ -25,6 +25,7 @@ import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.sample.Listener;
/**
@@ -38,13 +39,21 @@ public abstract class DecoderState extends AbstractDecoderState
private MutableIdentifierCollection mIdentifierCollection;
protected Listener<IdentifierUpdateNotification> mConfigurationIdentifierListener;
protected IChannelDescriptor mCurrentChannel;
private long mCurrentFrequency;
/**
* Constructs an instance
* @param mutableIdentifierCollection to preload into this decoder state
*/
public DecoderState(MutableIdentifierCollection mutableIdentifierCollection)
{
mIdentifierCollection = mutableIdentifierCollection;
mIdentifierCollection.update(new DecoderTypeConfigurationIdentifier(getDecoderType()));
}
/**
* Constructs an instance using an empty identifier collection.
*/
public DecoderState()
{
this(new MutableIdentifierCollection());
@@ -54,10 +63,28 @@ public abstract class DecoderState extends AbstractDecoderState
@Override
public void start()
{
super.start();
//Broadcast the existing identifiers (as add events) so that they can be received by external listeners
mIdentifierCollection.broadcastIdentifiers();
}
/**
* Current frequency for this channel.
*/
public long getCurrentFrequency()
{
return mCurrentFrequency;
}
/**
* Sets the current frequency for this channel.
* @param frequency to set.
*/
public void setCurrentFrequency(long frequency)
{
mCurrentFrequency = frequency;
}
/**
* Registers the listener to receive identifier update notifications
*/
@@ -152,9 +179,13 @@ public abstract class DecoderState extends AbstractDecoderState
{
getIdentifierCollection().receive(identifierUpdateNotification);
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier)
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier cdci)
{
setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifierUpdateNotification.getIdentifier()).getValue());
setCurrentChannel(cdci.getValue());
}
else if(identifierUpdateNotification.getIdentifier() instanceof FrequencyConfigurationIdentifier fci)
{
setCurrentFrequency(fci.getValue());
}
}
}

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
@@ -90,9 +90,10 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
/**
* Constructs an instance
* @param channel configuration
* @param aliasModel for channel metadata and identifiers
* @param timeslots array of timeslot numbers to use
*/
public MultiChannelState(AliasModel aliasModel, Channel channel, int[] timeslots)
public MultiChannelState(Channel channel, AliasModel aliasModel, int[] timeslots)
{
super(channel);
@@ -164,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();
@@ -204,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
{
@@ -236,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-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
@@ -95,7 +95,7 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
private StateMonitoringSquelchController mSquelchController = new StateMonitoringSquelchController(0);
private DecoderStateNotificationEventCache mStateNotificationCache = new DecoderStateNotificationEventCache();
public SingleChannelState(AliasModel aliasModel, Channel channel)
public SingleChannelState(Channel channel, AliasModel aliasModel)
{
super(channel);
mChannelMetadata = new ChannelMetadata(aliasModel);
@@ -162,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
{
@@ -174,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,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
@@ -26,12 +26,20 @@ import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
import io.github.dsheirer.sample.Listener;
/**
* Base decoder state implementation for a multi-timeslot protocol that tracks the state for a single timeslot.
*/
public abstract class TimeslotDecoderState extends DecoderState
{
private int mTimeslot;
/**
* Constructs an instance
* @param timeslot to monitor/maintain state.
*/
public TimeslotDecoderState(int timeslot)
{
super(new MutableIdentifierCollection(timeslot));
@@ -39,6 +47,9 @@ public abstract class TimeslotDecoderState extends DecoderState
mConfigurationIdentifierListener = new TimeslotConfigurationIdentifierListener();
}
/**
* Monitored timeslot for this decoder state instance.
*/
protected int getTimeslot()
{
return mTimeslot;
@@ -82,6 +93,10 @@ public abstract class TimeslotDecoderState extends DecoderState
{
setCurrentChannel((IChannelDescriptor)identifier);
}
else if(identifier instanceof FrequencyConfigurationIdentifier fci)
{
setCurrentFrequency(fci.getValue());
}
}
}
}

View File

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

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-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;
@@ -35,13 +32,16 @@ import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigFactory;
import io.github.dsheirer.source.config.SourceConfigRecording;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import java.beans.Transient;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
@@ -55,9 +55,6 @@ import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.beans.Transient;
import java.util.Objects;
@JacksonXmlRootElement(localName = "channel")
public class Channel extends Configuration implements Listener<SourceEvent>
{
@@ -88,7 +85,7 @@ public class Channel extends Configuration implements Listener<SourceEvent>
private BooleanProperty mAutoStart = new SimpleBooleanProperty();
private IntegerProperty mAutoStartOrder = new SimpleIntegerProperty();
private boolean mSelected;
private TunerChannel mTunerChannel = null;
private List<TunerChannel> mTunerChannels = null;
private ChannelType mChannelType = ChannelType.STANDARD;
@@ -634,9 +631,8 @@ public class Channel extends Configuration implements Listener<SourceEvent>
updateFrequencies();
//Clear the tune channel object so that it can be recreated if the
//source configuration changes
mTunerChannel = null;
//Clear the tune channels object so that it can be recreated if the source configuration changes
mTunerChannels = null;
}
/**
@@ -680,35 +676,50 @@ public class Channel extends Configuration implements Listener<SourceEvent>
}
/**
* Convenience method to construct a tuner channel object representing a
* tuner or recording source frequency and bandwidth that can be used by
* application components for graphically representing this channel.
* Convenience method to construct a tuner channel object representing a tuner or recording source frequency and
* bandwidth that can be used by application components for graphically representing this channel.
*
* If the source configuration is not a tuner or recording, this method
* returns null.
* If the source configuration is not a tuner or recording, this method returns an empty list.
*/
@JsonIgnore
public TunerChannel getTunerChannel()
public List<TunerChannel> getTunerChannels()
{
if(mTunerChannel == null)
if(mTunerChannels == null && mSourceConfiguration != null)
{
if(mSourceConfiguration.getSourceType() == SourceType.TUNER)
{
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
mTunerChannels = new ArrayList<>();
mTunerChannel = new TunerChannel(config.getFrequency(),
mDecodeConfiguration.getChannelSpecification().getBandwidth());
}
else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
switch(mSourceConfiguration.getSourceType())
{
SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration;
mTunerChannel = new TunerChannel(config.getFrequency(),
mDecodeConfiguration.getChannelSpecification().getBandwidth());
case TUNER:
if(mSourceConfiguration instanceof SourceConfigTuner tunerConfig)
{
mTunerChannels.add(new TunerChannel(tunerConfig.getFrequency(),
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
}
break;
case TUNER_MULTIPLE_FREQUENCIES:
if(mSourceConfiguration instanceof SourceConfigTunerMultipleFrequency multiConfig)
{
for(long frequency: multiConfig.getFrequencies())
{
mTunerChannels.add(new TunerChannel(frequency,
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
}
}
break;
case RECORDING:
if(mSourceConfiguration instanceof SourceConfigRecording recordingConfig)
{
mTunerChannels.add(new TunerChannel(recordingConfig.getFrequency(),
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
}
break;
default:
mLog.warn("Unrecognized channel source type: " + mSourceConfiguration.getSourceType());
}
}
return mTunerChannel;
return mTunerChannels;
}
/**
@@ -717,9 +728,15 @@ public class Channel extends Configuration implements Listener<SourceEvent>
*/
public boolean isWithin(long minimum, long maximum)
{
TunerChannel tunerChannel = getTunerChannel();
for(TunerChannel tunerChannel: getTunerChannels())
{
if(tunerChannel.overlaps(minimum, maximum))
{
return true;
}
}
return tunerChannel != null && tunerChannel.overlaps(minimum, maximum);
return false;
}
/**

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
@@ -23,38 +23,85 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.springframework.stereotype.Component;
/**
* Channel Model
*/
@Component("channelModel")
public class ChannelModel implements Listener<ChannelEvent>
{
private ObservableList<Channel> mChannels = FXCollections.observableArrayList(Channel.extractor());
private ObservableList<Channel> mTrafficChannels = FXCollections.observableArrayList(Channel.extractor());
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
@Resource
private AliasModel mAliasModel;
/**
* Constructs an instance
*/
public ChannelModel()
public ChannelModel(AliasModel aliasModel)
{
mAliasModel = aliasModel;
//Register a listener to detect channel changes and broadcast change events to cause playlist save requests
ChannelListChangeListener changeListener = new ChannelListChangeListener();
mChannels.addListener(changeListener);
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

@@ -20,20 +20,20 @@ package io.github.dsheirer.controller.channel;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioManager;
import io.github.dsheirer.audio.call.AudioSegment;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.channel.metadata.ChannelAndMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
import io.github.dsheirer.channel.state.AbstractChannelState;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
import io.github.dsheirer.map.MapService;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.module.decode.DecoderFactory;
@@ -51,10 +51,9 @@ import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.util.ThreadPool;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.awt.GraphicsEnvironment;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -66,7 +65,6 @@ import java.util.concurrent.locks.ReentrantLock;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Channel processing manager handles all starting and stopping of channel decoding. A processing chain is created
@@ -74,49 +72,54 @@ import org.springframework.stereotype.Component;
* channel and protocol along with all logging and baseband or bitstream recording. Audio recording is handled outside
* of this class by the RecorderManager.
*/
@Component("channelProcessingManager")
public class ChannelProcessingManager implements Listener<ChannelEvent>
{
private static final String DIVIDER = "-------------------------------------------------------------------------\n";
private final static Logger mLog = LoggerFactory.getLogger(ChannelProcessingManager.class);
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
private Map<Channel,ProcessingChain> mProcessingChains = new ConcurrentHashMap<>();
private Map<Channel,ProcessingChain> mProcessingChainsMap = new ConcurrentHashMap<>();
private Lock mLock = new ReentrantLock();
private ChannelSourceEventErrorListener mSourceErrorListener = new ChannelSourceEventErrorListener();
private List<Listener<AudioSegment>> mAudioSegmentListeners = new CopyOnWriteArrayList<>();
private List<Listener<IDecodeEvent>> mDecodeEventListeners = new CopyOnWriteArrayList<>();
private Broadcaster<ChannelEvent> mChannelEventBroadcaster = new Broadcaster();
private ChannelMapModel mChannelMapModel;
private ChannelMetadataModel mChannelMetadataModel;
private EventLogManager mEventLogManager;
private TunerManager mTunerManager;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private List<Long> mLoggedFrequencies = new ArrayList<>();
private List<ScheduledFuture<?>> mDelayedChannelStartTasks = new ArrayList<>();
@Resource
private AudioManager mAudioManager;
@Resource
private AliasModel mAliasModel;
@Resource
private ChannelMetadataModel mChannelMetadataModel;
@Resource
private DecoderFactory mDecoderFactory;
@Resource
private EventLogManager mEventLogManager;
@Resource
private MapService mMapService;
@Resource
private TunerManager mTunerManager;
@Resource
private UserPreferences mUserPreferences;
/**
* Constructs the channel processing manager
*
* @param channelMapModel containing channel maps defined by the user
* @param eventLogManager for adding event loggers to channels
* @param tunerManager for obtaining a tuner channel source for the channel
* @param aliasModel for aliasing of identifiers produced by the channel
* @param userPreferences for user defined behavior and settings
*/
public ChannelProcessingManager()
public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager,
TunerManager tunerManager, AliasModel aliasModel, UserPreferences userPreferences)
{
mChannelMapModel = channelMapModel;
mEventLogManager = eventLogManager;
mTunerManager = tunerManager;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mChannelMetadataModel = new ChannelMetadataModel();
}
@PostConstruct
public void postConstruct()
/**
* Channel metadata model containing metadata for each channel or channel time-slice that is currently processing.
*/
public ChannelMetadataModel getChannelMetadataModel()
{
addAudioSegmentListener(mAudioManager);
addDecodeEventListener(mMapService);
return mChannelMetadataModel;
}
/**
@@ -131,7 +134,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
isProcessing = mProcessingChains.containsKey(channel) && mProcessingChains.get(channel).isProcessing();
isProcessing = mProcessingChainsMap.containsKey(channel) && mProcessingChainsMap.get(channel).isProcessing();
}
finally
{
@@ -147,7 +150,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
public boolean isProcessing()
{
return !mProcessingChains.isEmpty();
return !mProcessingChainsMap.isEmpty();
}
/**
@@ -156,7 +159,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
public ProcessingChain getProcessingChain(Channel channel)
{
return mProcessingChains.get(channel);
return mProcessingChainsMap.get(channel);
}
/**
@@ -175,7 +178,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChainsMap.entrySet())
{
if(entry.getValue() == processingChain)
{
@@ -206,7 +209,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChainsMap.entrySet())
{
if(entry.getValue().hasSource(tunerChannelSource))
{
@@ -276,15 +279,27 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
break;
case REQUEST_DISABLE:
case NOTIFICATION_DELETE:
if(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;
@@ -379,8 +394,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
String threadName = "sdrtrunk channel [" + channel.getChannelID() + "/" +
channel.getDecodeConfiguration().getDecoderType().getShortDisplayString() + "]";
source = mTunerManager.getSource(channel.getSourceConfiguration(),
channel.getDecodeConfiguration().getChannelSpecification());
channel.getDecodeConfiguration().getChannelSpecification(), threadName);
}
catch(SourceException se)
{
@@ -390,7 +407,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
if(source == null)
{
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(false));
if(!GraphicsEnvironment.isHeadless())
{
Platform.runLater(() -> channel.setProcessing(false));
}
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel,
ChannelEvent.Event.NOTIFICATION_PROCESSING_START_REJECTED, TUNER_UNAVAILABLE_DESCRIPTION));
@@ -398,7 +418,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
throw new ChannelException("No Tuner Available");
}
ProcessingChain processingChain = new ProcessingChain(mAliasModel, channel);
ProcessingChain processingChain = new ProcessingChain(channel, mAliasModel);
//Certain decoders aggregate the decode events in the parent channel that also includes any events produced
//by the traffic channels. Establish listener registration depending on if this channel is a traffic channel
@@ -441,8 +461,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
processingChain.addFrequencyChangeListener(channel);
/* Processing Modules */
List<Module> modules = mDecoderFactory.getModules(channel, request.getTrafficChannelManager(),
request.getChannelDescriptor());
List<Module> modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences,
request.getTrafficChannelManager(), request.getChannelDescriptor());
processingChain.addModules(modules);
//Post preload data from the request to the event bus. Modules that can handle preload data will annotate
@@ -513,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())
{
@@ -556,11 +583,11 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
if(!mProcessingChains.containsKey(channel))
if(!mProcessingChainsMap.containsKey(channel))
{
added = true;
mProcessingChains.put(channel, processingChain);
mChannelMetadataModel.add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
mProcessingChainsMap.put(channel, processingChain);
getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
}
}
finally
@@ -584,13 +611,13 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
removed = mProcessingChains.remove(channel);
removed = mProcessingChainsMap.remove(channel);
if(removed != null)
{
for(ChannelMetadata channelMetadata: removed.getChannelState().getChannelMetadata())
{
mChannelMetadataModel.remove(channelMetadata);
getChannelMetadataModel().remove(channelMetadata);
}
}
}
@@ -619,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
@@ -633,6 +680,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
processingChain.removeFrequencyChangeListener(channel);
channel.resetFrequencyCorrection();
//Notify all processing chains that this channel is shutting down so that if this is a traffic channel,
//the owning parent channel's traffic channel manager can cleanup it's accounting.
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
mChannelEventBroadcaster.removeListener(processingChain);
@@ -660,7 +709,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
mDelayedChannelStartTasks.remove(delayedTask);
}
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
List<Channel> channelsToStop = new ArrayList<>(mProcessingChainsMap.keySet());
for(Channel channel : channelsToStop)
{
@@ -683,7 +732,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
public void convertToTrafficChannel(ChannelConversionRequest request)
{
//Update the channel to processing chain map.
ProcessingChain processingChain = mProcessingChains.remove(request.getCurrentChannel());
ProcessingChain processingChain = mProcessingChainsMap.remove(request.getCurrentChannel());
if(processingChain != null)
{
@@ -697,7 +746,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
request.getTrafficChannel().setProcessing(true);
});
mProcessingChains.put(request.getTrafficChannel(), processingChain);
mProcessingChainsMap.put(request.getTrafficChannel(), processingChain);
mChannelMetadataModel.updateChannelMetadataToChannelMap(processingChain.getChannelState().getChannelMetadata(),
request.getTrafficChannel());
@@ -803,7 +852,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChains.entrySet())
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChainsMap.entrySet())
{
if(entry.getValue().hasSource(sourceEvent.getSource()))
{
@@ -837,8 +886,64 @@ 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);
}
}
}
}
}
/**
* Creates a diagnostic report.
* @return report text.
*/
public String getDiagnosticInformation()
{
StringBuilder sb = new StringBuilder();
sb.append("Channel Processing Manager - Diagnostics Report\n\n");
sb.append(DIVIDER);
sb.append("\tChannel to Processing Chain Map Contents\n");
Map<Channel,ProcessingChain> mapCopy = new HashMap<>(mProcessingChainsMap);
for(Map.Entry<Channel,ProcessingChain> entry: mapCopy.entrySet())
{
sb.append("\n\n--------------- CHANNEL:PROCESSING CHAIN MAP ENTRY --------------------\n");
try
{
Channel channel = entry.getKey();
sb.append("\tChannel: " + channel).append("\n");
sb.append("\t\tSource Configuration: " + channel.getSourceConfiguration()).append("\n");
ProcessingChain chain = entry.getValue();
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");
}
Source source = chain.getSource();
if(source != null)
{
sb.append("Channel Source Class: " + source.getClass()).append("\n");
sb.append("\t\tTo String:").append(source).append("\n");
sb.append("\t\tHash:").append(Integer.toHexString(source.hashCode()).toUpperCase()).append("\n");
}
else
{
sb.append("Channel Source: (null)\n");
}
}
catch(Throwable t)
{
sb.append("\tError while logging diagnostics of map entry - " + t.getMessage()).append("\n");
}
}
return sb.toString();
}
}

View File

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

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