Compare commits

..

79 Commits

Author SHA1 Message Date
Denny Sheirer
421da1c1ce Cherry Pick: #1405 polyphase channelizer thread leak. 2023-03-05 06:34:29 -05:00
Denny Sheirer
2412833899 Updates release to 0.5.2 2023-03-05 06:32:56 -05:00
Denny Sheirer
9abf64c6ab #1449 Updates version to 0.5.1 final (#1451)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-19 07:17:10 -05:00
Denny Sheirer
1702103f5d #1449 Updates version to 0.5.1 beta-2 (#1450)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-19 07:10:44 -05:00
Denny Sheirer
8f68a989b9 #1444 Adds: (#1447)
* DMR Recording viewer utility
* DMR talker aliases
* DMR SMS messages
* DMR Hytera Tier-III compressed talkgroups support
* DMR UDT packet sequences
* DMR Hytera Aloha and other unknown CSBK and Link Control messages

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-18 08:36:55 -05:00
Denny Sheirer
b51a0102ef #1422 Resolves issue where MotoTRBO IP Site Connect and Capacity Plus voice group user message not decoding Radio & Talkgroup IDs correctly. Creates standalone application for viewing DMR messages from recordings. (#1442)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-02-11 15:32:41 -05:00
Denny Sheirer
e177c2c6b0 #1426 Adds JRE accessibility module to support assistive technologies. (#1427)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2023-01-21 07:21:00 -05:00
Denny Sheirer
3567fdf51d #1393 Updates DMR Talkgroup and Radio ID editors to use correct formatting. Patch submitted by @hockeyref2 (#1425)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2023-01-20 06:50:18 -05:00
Denny Sheirer
323097ba65 #1423 Updates Icecast/Broadcastify feed to attempt reconnect following a mountpoint in use error response. (#1424)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2023-01-20 05:39:38 -05:00
Denny Sheirer
33f2bab7ac #1419 Reduce logging from USBTunerController for (temporary) failed transfer buffer resubmits. (#1420)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-18 04:38:30 -05:00
Denny Sheirer
c063cfb201 #1416 Updates P25-P1 To Expose ICMP & Unknown Packet Events in Event History View (#1417)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-16 05:31:27 -05:00
Denny Sheirer
e99aa3d3b4 #1411 Updates version to 0.5.0 beta1 (#1412)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-15 06:36:22 -05:00
Denny Sheirer
ec162ff84c #1408 Updates USB tuners to enhance transfer buffer error handling and logging. When an error occurs while (re)submitting a transfer buffer to LibUsb to fill with sample data, the buffer is temporarily placed in an error queue. Upon successfully submitting the next transfer buffer, we attempt to resubmit a buffer from the error queue, so that we wait until the device can accept buffers before trying to resubmit. If all transfer buffers are exhausted because they are in the error queue, then the tuner gets shutdown with an error state ... there's really no way to recover at this point. (#1409)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-15 06:16:37 -05:00
Denny Sheirer
f0db2865f2 #1397 Resolves issue where decoding channels go IDLE after period of time. Isolated the cause to a multi-threading issue where the polyphase channel manager receives notification that the tuner center frequency has changed and sets a flag to update each of the channels. This happens concurrent with a previous tuner center frequency change where the flag gets set and the second thread is updating the output processors with the previous frequency change and then unsets the frequency change flag, completely missing the second/latest frequency change update and causing the channels to all tune-out their desired signals as the tuner center frequency changes and the output channels continue mixing to the previous frequency. (#1407)
Identified and resolved issue where a 2-stream polyphase synthesizer was being updated with an incorrectly designed synthesis filter which was causing massive droop on the second half of the reconstructed channel.  Oddly enough, this didn't seem to impact the P25 decoders, but it may have impacted other decoding.

Identified and resolved issue with concurrent modification of the modules list in the processing chain associated with recent addition of the channel FFT view in the channel power tab.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-15 04:12:42 -05:00
Denny Sheirer
f97b2150b9 #1397 Adds channel fft frequency overlay, cursor readout, pll tracking indicator, and channel bandwidth lines. (#1406)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-13 04:28:00 -05:00
Denny Sheirer
8ce8aaddf9 #1399 Resolves device ID parsing issue for Motorola ARS Device Registration. (#1400)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2023-01-06 04:17:57 -05:00
Denny Sheirer
e4a85a4ed7 #1386 Updates version to 0.5.0 (final) (#1387)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-25 07:11:25 -05:00
Denny Sheirer
f61a415b43 #1383 Updates tuner editor to resolve sequencing issue where a recently enabled tuner was having its enabled state set to true and then enabling the tuner, which caused the tuner editor to mis-configure the tuner editor panel. (#1385)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-25 07:03:28 -05:00
Denny Sheirer
29ed3b4553 #1287 Disables event panel filtering until the feature is fully implemented with #1368. The current partial implementation is preventing valid events from displaying for protocols like DMR and likely others. (#1384)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-25 06:45:50 -05:00
Denny Sheirer
a87309e792 #1378 Resolves potential error where tuner shutdown (because of USB I/O error) hangs and causes application to run out of memory. This patch doesn't resolve the original issue with the USB I/O error, but it does ensure that the tuner shutdown which is happening on the USB processing thread, can complete in a timely fashion so that the application doesn't (potentially) crash from out of memory. (#1382)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-24 09:10:17 -05:00
Denny Sheirer
ddb5b5ef87 #1380 Improves Icecast streaming error handling and preserves streaming error messages for broadcast state to display in streaming status panel. Resolves issue with streaming status panel not showing icons and stream type. (#1381)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-24 07:48:04 -05:00
Denny Sheirer
7a58bf85b6 #1312 Resolves issue with radio reference editor attempting to access county info for a site that has county ID set to 0 (ie no county specified). (#1379)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-24 04:45:21 -05:00
Doug Hoffman
8707eb2c88 Correctly identify padded MP3 frames (#1375) 2022-12-23 06:12:44 -05:00
Denny Sheirer
08499ac73f #1237 Ensure MP3 input sample rate is supported by LAME encoder settings. (#1376)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-23 06:06:10 -05:00
Dennis Sheirer
70170ff70f Revert "Disable resampling if requested sample rate not supported"
This reverts commit 7de28519a1.
2022-12-18 08:29:35 -05:00
Denny Sheirer
bad5589c0d #1369 Updates to Version 0.5.0 Beta 7 (#1370)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-18 07:17:23 -05:00
Doug Hoffman
7de28519a1 Disable resampling if requested sample rate not supported 2022-12-18 04:57:21 -05:00
Doug Hoffman
bbd907514e Handle streaming audio as frames (#1244) 2022-12-18 04:46:07 -05:00
Denny Sheirer
4a68cd9407 #1364 Enhancements to resolve issue with tuner shutdown hang on application shutdown. (#1365)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-18 04:29:55 -05:00
Denny Sheirer
dc327a2855 #1362 Updates heterodyne channelizer with alternate sorted set implementation to resolve issue when determining if the tuner can tune a requested channel. (#1363)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-17 09:28:29 -05:00
Denny Sheirer
9af593a785 #1360 Resolves issue with Polyphase channelizer and decoded message timestamps for P25 and other protocols. (#1361)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-17 08:50:29 -05:00
Denny Sheirer
24f4c22cdd #1358 Resolves issue where decoded AMBE (DMR & P25-2) tone identifiers are not sent to the alias action manager. (#1359)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-17 07:35:16 -05:00
Denny Sheirer
bc462b9652 #1351 Restores semi-accurate timestamps of sample buffers to ensure P25 & DMR decoded messages have accurate timestamps and MBE recording voice frames have accurate timestamps. (#1357)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-17 06:39:35 -05:00
Denny Sheirer
047dec6da1 #1355 Updates logging libraries to latest version and updates logging libraries to latest version. (#1356)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-17 04:06:44 -05:00
Denny Sheirer
bf935555bb #1353 Resolves issue where polyphase channelizer would mis-tune channels after a period of time resulting in idle decoding channels. (#1354)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-12-17 03:54:50 -05:00
Dennis Sheirer
efe36b0682 Updates README 2022-12-03 09:29:20 -05:00
Denny Sheirer
a18d4498aa #1346 Updates radio reference library to version 15.1.9 (#1347)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-11-25 09:11:10 -05:00
Denny Sheirer
803dc2531c #1318 Resolves issue where bitstream recorder no longer works. Solution submitted by @apankowski. (#1345)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-11-24 06:16:44 -05:00
Denny Sheirer
1b062bcd06 #1343 Resolves issue where user-initiated FFT size change causes waterfall panel pixel array to be updated/resized while a memory image source repaint has been requested and the parameters become incongruent, causing an array index out of bounds error. Pushes the pixel array update operation onto the swing thread to ensure only a single thread is modifying the pixel map for rendering. (#1344)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-11-24 04:23:48 -05:00
Denny Sheirer
a24c3736f4 #1340 Updates tuner and frequency controllers to apply locking to ensure multi-threaded access to freuqency control planes doesn't allow center tuned frequency to become indeterminant which can result in channels being mistuned and stop decoding. (#1341)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-11-20 07:55:30 -05:00
Denny Sheirer
ac61b681e6 #1338 Updates logger to include date in log entries. Removes info logger when creating channel log files. (#1339)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-11-20 05:54:05 -05:00
Denny Sheirer
0866bfd555 #1336 Resolves issue with color setting changes not inheriting the correct translucency which causes the channel overlays in the spectral display to be solid color instead of semi-transparent. (#1337)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-11-20 05:43:28 -05:00
Denny Sheirer
79fed701d9 #1337 Updates build to use JDK19 for distribution images and updates all 3rd-party libraries to latest. Note: build system continues to be compatible with JDK 17 and 18, but Gradle 7.6 to build distribution images. Gradle 7.6 is not released yet, but RC3 is available. (#1335)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-11-20 05:36:41 -05:00
Denny Sheirer
48a93990e8 #1291 Updates DC removal processing across tuner sample streams to run on a periodic timer to reduce overall processing requirements. Updates build version to 0.5.0 beta 5 (#1292)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-07-23 08:14:31 -04:00
Denny Sheirer
414ba25d61 #1284 Resolves issue with inherent DC bias (-0.5) in RTL2832 sample conversion code. (#1285)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-07-16 08:58:09 -04:00
Shahar
f67f90e8b0 moved RecordingTuner ChannelizerType set to after the recording tuner was started. This way the sample rate is not 0. (#1279) 2022-07-11 04:34:17 -04:00
Dennis Sheirer
5751c70212 Updates to release versio 0.5.0 2022-07-11 04:21:03 -04:00
Denny Sheirer
4e15c426a8 #1253 Updates USB tuner controller to handle LibUsb bug where on resubmitting a completed transfer LibUsb tells us that the transfer is already submitted. This is happening on some Window systems. (#1263)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-24 04:11:26 -04:00
Denny Sheirer
ceda189260 #1179 Resolves issue with JMBE Creator running on OSX. (#1262)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-24 03:57:33 -04:00
Denny Sheirer
fce0ae33e7 #1181 Updates libusb osx aarch64 driver library and gradle build. (#1261)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-23 04:06:42 -04:00
Denny Sheirer
fc5c6ef841 #1257 restores recording tuner feature. sdrtrunk now supports multiple recording tuners. Recordings are automatically loaded/restored on application start and each recording tuner has to be manually enabled for use. (#1258)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-22 08:24:42 -04:00
Denny Sheirer
d0033578d5 #1181 Adds logging for mac users to install libusb if sdrtrunk can't load library. (#1251)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-15 07:05:53 -04:00
Dennis Sheirer
670ec3a097 Adds debug logging to USB tuner controller. 2022-05-15 06:37:25 -04:00
Denny Sheirer
8bfb1d5470 #1243 Updates USB tuner error handling to prevent stack overflow when tuner error occurs while streaming. (#1247)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-09 07:16:35 -04:00
Denny Sheirer
46a916a945 #1245 Updates spectral display show tuner options to allow new display without changing disabled state of primary display. (#1246)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-08 05:01:52 -04:00
Denny Sheirer
459e77390a #1241 Adds user preference to enable/disable audio normalization prior to MP3 encoding/recording. (#1242)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-07 07:11:40 -04:00
Doug Hoffman
f4ab46b79d Prefer talkgroup ID over radio ID for Calls (#1227) 2022-05-07 06:51:27 -04:00
Denny Sheirer
3c58e98788 #1239 Resolves issue where a user can't view the spectrum of a tuner when the spectral display is disabled. Tuner editor button overrides the property setting so that the view completes correctly. (#1240)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-07 06:36:42 -04:00
Doug Hoffman
36de81248c Don't show tuner on spectral display while disabled (#1236) 2022-05-07 06:30:11 -04:00
Dennis Sheirer
925353d515 Bump Version 0.5.0 Beta 3, Gradle 7.4.2, Embedded JDK 18.0.1 2022-05-04 14:51:27 -04:00
Denny Sheirer
7c32897321 #1229 Resolves issue with correctly identifying the port number for USB tuners that is causing some tuners not to be recognized and also generating udev errors when sdrtrunk attempts to start an already started device because it was incorrectly identified. (#1230)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-05-03 04:03:18 -04:00
Doug Hoffman
cbeda19d27 Generate audio metadata with correct to/from (#1215)
#1215 Generate audio metadata with correct to/from, preferring patch group, then talkgroup then other, for TO talkgroups.
2022-05-01 06:06:31 -04:00
Doug Hoffman
8148ca48af Normalize audio before recording (#1213)
#1213 Normalize audio before recording
2022-05-01 05:57:32 -04:00
VolFFProCoder
adcb55ffbc Add filter button to messages (#1098)
Adds filtering to channel decoded events panel.
2022-04-30 05:44:57 -04:00
Andrzej Pańkowski
1152b13220 Merge pull request #1177
* Fixes message filtering that was not being initialized.  Fixes minor spotted code issues
2022-04-30 04:25:33 -04:00
Doug Hoffman
166383e1ed Make Broadcastify Calls scheduled test configurable (#1218)
* Broadcastify Calls periodic keep-alive to avoid showing offline status
2022-04-30 04:04:10 -04:00
Denny Sheirer
0d5d1b20e3 #1223 Resolves NPE when Airspy and HackRF tuners are re-enabled after startup. Slight tweaks to TunerManager to change when discovered USB device startup takes place to potentially address intermittent fail on start for some tuners that users are seeing. Resolves issue where a started device was not unreferenced in LibUsb on shutdown. (#1224)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-04-29 08:00:18 -04:00
Denny Sheirer
77ea43f61a #1221 resolves null pointer exception in tuner editor when attempting to restart an error tuner. (#1222)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-04-28 04:40:52 -04:00
Denny Sheirer
f988344222 #1219 resolves issues with NPE and restarting an error tuner. (#1220)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-04-26 04:02:36 -04:00
Doug Hoffman
60489e1fef (icecast) Add support for inline metadata (#1212) 2022-04-25 04:38:54 -04:00
Doug Hoffman
ac8aa17a80 Check in with Broadcastify Calls to stay online while otherwise idle (#1214) 2022-04-24 05:15:34 -04:00
Denny Sheirer
25a42126df #1210 Resolves issue of tuner instances can't be garbage collected due to retained references. Updates several of the tuner controller and tuner editor classes to add/invoke dispose() methods to clear retained instance references. (#1216)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2022-04-24 04:56:10 -04:00
Denny Sheirer
1b90e0b50c #1209 updates tuner manager to properly deref usb devices on discovery. Updates tuner editors to separate enable/disable from tuner error state. Updates settings manager to disable auto-fail when loading xml file when unknown tags are encountered so that users' existing configs don't fail on startup and erase their existing settings. (#1211)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2022-04-23 06:05:12 -04:00
Denny Sheirer
7f5ad7eb20 Update README.md 2022-04-19 07:03:19 -04:00
Denny Sheirer
0e6713b6e2 Update README.md 2022-04-19 07:02:32 -04:00
Denny Sheirer
96d3996bec #1190 Tuner management redesign. Updates all tuners with direct threaded support. Incorporates USB Hotplug support. Adds black/white listing of tuners. Updates all tuner editors to have a common look and feel. Changes location of tuner configuration file and changes format to JSON. (#1208)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-04-18 07:30:58 -04:00
Denny Sheirer
0fb148d31e #1194 Resolves issue with audio stream bit rate scheduling that was causing stream disconnects for data rate under flows. (#1195)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-03-26 05:48:08 -04:00
Denny Sheirer
cebca28b19 Patch from @ImagoTrigger to implement headless operation: 9e4d2adfa9..c597d1967b (#1191)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2022-03-25 04:19:38 -04:00
Denny Sheirer
af8cc0255e #1186 removes error logging that indicates that a channel cannot be stopped because it doesn't exist. This happens when a second successive attempt to stop a channel fails to find the channel because the first attempt has already removed the channel from the list. (#1187)
Co-authored-by: Denny Sheirer <sheirerd@ainfosec.com>
2022-03-21 19:09:49 -04:00
493 changed files with 22842 additions and 16349 deletions

5
.idea/misc.xml generated
View File

@@ -1,7 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="liberica-17.0.2-fx" project-jdk-type="JavaSDK">
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="liberica-19.0.1.fx" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -3,91 +3,22 @@
# sdrtrunk
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted_V0.3.0)
* [User's Manual Version 0.3.0 and 0.4.0](https://github.com/DSheirer/sdrtrunk/wiki/UserManual_V0.3.0)
* [Playlist Editor User Manual Version 0.5.0](https://github.com/DSheirer/sdrtrunk/wiki/Playlist-Editor)
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/Getting-Started)
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
* [Support Group](https://groups.google.com/forum/#!forum/sdrtrunk)
* [Discord Support Channel](https://discord.gg/HJQaKYE)
* [Support](https://github.com/DSheirer/sdrtrunk/wiki/Support)
![sdrtrunk Application Overview - Version 0.3.0](https://github.com/DSheirer/sdrtrunk/wiki/v0.3/images/ApplicationOverview_V0.3.0.png)
**Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot
![sdrtrunk Application](https://github.com/DSheirer/sdrtrunk/wiki/images/sdrtrunk.png)
**Figure 1:** sdrtrunk Application Screenshot
# End User Instructions:
If you simply want to download and run the program, please follow these instructions.
## Download the latest sdrtrunk release for your operating system
## Download the Latest Release
All release versions of sdrtrunk are available from the [releases](https://github.com/DSheirer/sdrtrunk/releases) tab.
* **(alpha)** These versions are under development feature previews and likely to contain bugs and unexpected behavior.
* **(beta)** These versions are currently being tested for bugs and functionality prior to final release.
* **(final)** These versions have been tested and are the current release version.
## Unzip the release
Use 7-zip or any zip utility to unzip the release file
## Start the application
Once unzipped, open a command prompt to where you unzipped the release. Change to the **/bin** directory and use the launch script to start the application:
* **Windows** sdr-trunk.bat
* **Linux/OSX** ./sdr-trunk
## Optional - P25/DMR Audio
If you're using sdrtrunk with a P25 or DMR radio system, you'll need to compile and include the [JMBE](https://github.com/DSheirer/sdrtrunk/wiki/JMBE) digital audio library.
In Version 0.5.0 and higher, the SDRTrunk application has support for building the JMBE library. On the menu, choose **View >> User Preferences** and then click the **Decoder >> JMBE Audio Library** tab.
For versions prior to Version 0.5.0, the [JMBE](https://github.com/DSheirer/sdrtrunk/wiki/JMBE) wiki page contains instructions for downloading the JMBE audio library source code and compiling the JMBE library. Once you have compiled the library, launch the sdrtrunk application. From the menu bar, choose **View >> Preferences**. In the **JMBE Audio Codec** section, update the path to where your compiled JMBE library is located. Any channels that are started after you set the path will be able to produce P25 audio.
## Minimum System Requirements
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **CPU:** 4-core
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.
# Developer Instructions:
If you're interested in modifying and/or compiling the source code, please follow these instructions to use gradle to compile the code.
## Build the project
sdrtrunk uses the gradle build system. This requires **OpenJDK 17** or higher installed on your local computer.
### OpenJDK
There are many vendors producing OpenJDK distributions. A few that I use are:
* [Azul Zulu OpenJDK](https://www.azul.com/downloads/?package=jdk)
* [Bellsoft Liberica OpenJDK](https://bell-sw.com/pages/downloads/#/java-17-lts%20/%20current)
* [SDKMAN](https://sdkman.io/) - Linux Only
Use the gradle wrapper to build the source code:
### Linux
```
./gradlew clean build
```
### Windows
```
gradlew.bat clean build
```
The **/build/distributions** folder will contain the zip file of the compiled program. Unzip it and launch the program from the scripts in the **/bin** directory.
## Run the Application (from Gradle)
### Linux
```
./gradlew clean run
```
### Windows
```
gradlew.bat clean run
```
## Development
All dependencies/versions are controlled from build.gradle.
To change the new release version tag of artifact - change property:
```
version = '0.5.0'
```

View File

@@ -28,7 +28,7 @@ repositories {
maven { url "https://jitpack.io" }
}
version = '0.5.0-beta2'
version = '0.5.2'
sourceCompatibility = '17'
sourceSets {
@@ -43,10 +43,10 @@ test {
dependencies {
// JUnit Tests
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.8.0'
testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
testImplementation 'org.mockito:mockito-core:3.+'
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'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
@@ -56,37 +56,37 @@ dependencies {
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
//mavenCentral/jcenter imports
implementation 'ch.qos.logback:logback-classic:1.2.3'
implementation 'ch.qos.logback:logback-core:1.2.3'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.8'
implementation 'com.fazecast:jSerialComm:2.5.0'
implementation 'ch.qos.logback:logback-classic:1.4.5'
implementation 'ch.qos.logback:logback-core:1.4.5'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
implementation 'com.fazecast:jSerialComm:2.9.3'
implementation 'com.github.jiconfont:jiconfont-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.8.6'
implementation 'com.google.guava:guava:27.0.1-jre'
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.google.guava:guava:31.1-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:5.2'
implementation 'com.miglayout:miglayout-swing:11.0'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.7'
implementation 'commons-io:commons-io:2.11.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:radio-reference-api:15.1.8'
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.4.0'
implementation 'org.apache.commons:commons-compress:1.20'
implementation 'org.apache.commons:commons-lang3:3.8.1'
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
implementation 'org.apache.commons:commons-compress:1.21'
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.mina:mina-core:2.1.3'
implementation 'org.apache.mina:mina-http:2.1.3'
implementation 'org.controlsfx:controlsfx:11.1.0'
implementation 'org.rauschig:jarchivelib:1.0.0'
implementation 'org.slf4j:slf4j-api:1.7.25'
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.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.0' //usb4java native lib for OSX M1 cpu
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
@@ -139,11 +139,11 @@ String targetWindowsX86_64 = 'windows-x86_64-v' + version
* author's development environment, but can be changed to your environment if you want to use this build pattern.
*/
String jdk_base = '/home/denny/java_jdks/'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-17.0.2-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-17.0.2-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-17.0.2-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-17.0.2-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-17.0.2-full'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-19.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-19.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-19.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-19.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-19.0.1-full'
boolean hasTargetJdks = file(jdk_linux_x86_64).exists() || file(jdk_linux_aarch64).exists() ||
file(jdk_osx_x86_64).exists() || file(jdk_osx_aarch64).exists() || file(jdk_windows_x86_64).exists()
@@ -172,19 +172,19 @@ runtime {
println("Building all runtime images")
println("The [ > jre: ] build phase downloads several target JDKs and this may take a while ... please wait")
targetPlatform(targetLinuxAarch64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/17.0.2+9/bellsoft-jdk17.0.2+9-linux-aarch64-full.tar.gz")
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-linux-aarch64-full.tar.gz")
}
targetPlatform(targetLinuxX86_64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/17.0.2+9/bellsoft-jdk17.0.2+9-linux-amd64-full.tar.gz")
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-linux-amd64-full.tar.gz")
}
targetPlatform(targetOsxAarch64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/17.0.2+9/bellsoft-jdk17.0.2+9-macos-aarch64-full.tar.gz")
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-macos-aarch64-full.tar.gz")
}
targetPlatform(targetOsxX86_64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/17.0.2+9/bellsoft-jdk17.0.2+9-macos-amd64-full.tar.gz")
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-macos-amd64-full.tar.gz")
}
targetPlatform(targetWindowsX86_64) {
jdkHome = jdkDownload("https://download.bell-sw.com/java/17.0.2+9/bellsoft-jdk17.0.2+9-windows-amd64-full.zip")
jdkHome = jdkDownload("https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-windows-amd64-full.zip")
}
}
//Build using pre-downloaded JDKs
@@ -212,7 +212,9 @@ runtime {
}
//Additional modules to include with auto-detected modules.
modules = ['jdk.crypto.ec'] //jdk.crypto.ec needed for HTTPS connections (broadcastify calls & map tile server)
//jdk.crypto.ec needed for HTTPS connections (broadcastify calls & map tile server)
//jdk.accessibility is used with assistive technologies like screen readers
modules = ['jdk.crypto.ec','jdk.accessibility']
//Use auto-detected modules and 'add' any specified modules.
additive = true

Binary file not shown.

View File

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

306
gradlew vendored
View File

@@ -1,94 +1,132 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# ******************************************************************************
# Copyright (C) 2014-2022 Dennis Sheirer
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# This 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.
#
# https://www.apache.org/licenses/LICENSE-2.0
# 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.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
# *****************************************************************************
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
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"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -97,7 +135,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
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.
Please set the JAVA_HOME variable in your environment to match the
@@ -105,79 +143,105 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
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
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

38
gradlew.bat vendored
View File

@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,10 +25,14 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@@ -37,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -51,7 +55,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -61,38 +65,26 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

View File

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

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ public class AudioSegmentBroadcaster<T extends AudioSegment> extends Broadcaster
@Override
public void broadcast(T audioSegment)
{
for(Listener<T> listener : mListeners)
for(Listener<T> listener : getListeners())
{
audioSegment.incrementConsumerCount();
listener.receive(audioSegment);

View File

@@ -24,7 +24,10 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.Float;
import java.lang.Math;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class AudioUtils
@@ -78,4 +81,43 @@ public class AudioUtils
return stream.toByteArray();
}
/**
* Normalize audio samples
*/
public static List<float[]> normalize(List<float[]> audioBuffers)
{
float targetMax = 0.95f; // -0.44dBFS
float max = 0.1f; // Initialize at 0.1 (-20dBFS) to limit maximum gain
for(float[] audioBuffer: audioBuffers)
{
for(float value: audioBuffer)
{
float newMax = Math.max(max, Math.abs(value));
if(!Float.isNaN(newMax))
{
max = newMax;
}
}
}
if(targetMax == max)
{
return audioBuffers;
}
float gain = targetMax / max;
for(float[] audioBuffer: audioBuffers)
{
for(int i=0;i<audioBuffer.length;i++)
{
audioBuffer[i] *= gain;
}
}
return audioBuffers;
}
}

View File

@@ -18,40 +18,45 @@
*/
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.audio.convert.AudioFrames;
import io.github.dsheirer.audio.convert.ISilenceGenerator;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3FrameTools;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.util.ThreadPool;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration> extends AbstractAudioBroadcaster<T>
{
private final static Logger mLog = LoggerFactory.getLogger(AudioStreamingBroadcaster.class);
public static final int PROCESSOR_RUN_INTERVAL_MS = 1000;
private ScheduledFuture mRecordingQueueProcessorFuture;
private ScheduledFuture<?> mRecordingQueueProcessorFuture;
private RecordingQueueProcessor mRecordingQueueProcessor = new RecordingQueueProcessor();
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private ISilenceGenerator mSilenceGenerator;
private BroadcastFormat mBroadcastFormat;
private long mDelay;
private long mMaximumRecordingAge;
private AtomicBoolean mStreaming = new AtomicBoolean();
protected boolean mInlineActive = false;
protected int mInlineInterval;
protected int mInlineRemaining = -1;
private int mTimeOverrun = 0;
/**
* AudioBroadcaster for streaming audio recordings to a remote streaming audio server. Audio recordings are
* generated by an internal StreamManager that converts an inbound stream of AudioPackets into a recording of the
@@ -76,10 +81,11 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
public AudioStreamingBroadcaster(T broadcastConfiguration, InputAudioFormat inputAudioFormat, MP3Setting mp3Setting)
{
super(broadcastConfiguration);
mBroadcastFormat = broadcastConfiguration.getBroadcastFormat();
mDelay = getBroadcastConfiguration().getDelay();
mMaximumRecordingAge = getBroadcastConfiguration().getMaximumRecordingAge();
mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat(),
inputAudioFormat, mp3Setting);
inputAudioFormat, mp3Setting);
}
public void dispose()
@@ -89,7 +95,7 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
/**
* Broadcast binary audio data frames or sequences.
*/
protected abstract void broadcastAudio(byte[] audio);
protected abstract void broadcastAudio(byte[] audio, IdentifierCollection identifierCollection);
/**
* Protocol-specific metadata updater
@@ -205,13 +211,15 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
super.setBroadcastState(state);
if(mBroadcastState.get() instanceof BroadcastState && ((BroadcastState)mBroadcastState.get()).isErrorState())
if(mBroadcastState.get() != null && mBroadcastState.get().isErrorState())
{
stop();
}
if(!connected())
{
//Reset inline metadata
mInlineRemaining = -1;
//Remove all pending audio recordings
while(!mAudioRecordingQueue.isEmpty())
{
@@ -243,7 +251,6 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
public boolean canConnect()
{
BroadcastState state = getBroadcastState();
return state != BroadcastState.CONNECTED && !state.isErrorState();
}
@@ -264,10 +271,8 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
public class RecordingQueueProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private ByteArrayInputStream mInputStream;
private long mFinalSilencePadding = 0;
private int mBytesStreamedActual = 0;
private int mBytesStreamedRequired = 0;
private AudioFrames mInputFrames;
private IdentifierCollection mInputIdentifierCollection;
@Override
public void run()
@@ -276,55 +281,35 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
{
try
{
if(mInputStream == null || mInputStream.available() <= 0)
int timeSent = 0;
if(mInputFrames == null || !mInputFrames.hasNextFrame())
{
if(mFinalSilencePadding > 0)
{
List<byte[]> finalSilence = mSilenceGenerator.generate(mFinalSilencePadding);
for(byte[] silence: finalSilence)
{
broadcastAudio(silence);
}
mFinalSilencePadding = 0;
}
nextRecording();
}
if(mInputStream != null)
if(mInputFrames != null && mInputFrames.hasNextFrame())
{
//We need to stream at 13.888 fps (144 byte frame) to achieve 2000 Bps or 16 kbps
mBytesStreamedRequired += 2000; //2000 bytes per second for 16 kbps data rate
int bytesToStream = mBytesStreamedRequired - mBytesStreamedActual;
//Trim length to whole-frame intervals (144 byte frame)
bytesToStream -= (bytesToStream % 144);
int length = FastMath.min(bytesToStream, mInputStream.available());
byte[] audio = new byte[length];
try
while(mInputFrames.hasNextFrame() && timeSent < PROCESSOR_RUN_INTERVAL_MS)
{
mBytesStreamedActual += mInputStream.read(audio);
broadcastAudio(audio);
}
catch(IOException ioe)
{
mLog.error("Error reading from in-memory audio recording input stream", ioe);
mInputFrames.nextFrame();
broadcastAudio(mInputFrames.getCurrentFrame(), mInputIdentifierCollection);
timeSent += mInputFrames.getCurrentFrameDuration();
}
}
else
if((mInputFrames == null || !mInputFrames.hasNextFrame()) && timeSent < PROCESSOR_RUN_INTERVAL_MS)
{
List<byte[]> silenceFrames = mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS);
for(byte[] silence: silenceFrames)
AudioFrames silenceFrames = mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS - mTimeOverrun - timeSent);
while(silenceFrames.hasNextFrame())
{
broadcastAudio(silence);
silenceFrames.nextFrame();
broadcastAudio(silenceFrames.getCurrentFrame(), null);
timeSent += silenceFrames.getCurrentFrameDuration();
}
}
mTimeOverrun += timeSent - PROCESSOR_RUN_INTERVAL_MS;
}
catch(Throwable t)
{
@@ -340,12 +325,9 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
*/
private void nextRecording()
{
mBytesStreamedActual = 0;
mBytesStreamedRequired = 0;
boolean metadataUpdateRequired = false;
if(mInputStream != null)
if(mInputFrames != null)
{
mStreamedAudioCount++;
broadcast(new BroadcastEvent(AudioStreamingBroadcaster.this,
@@ -353,7 +335,8 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
metadataUpdateRequired = true;
}
mInputStream = null;
mInputFrames = null;
mInputIdentifierCollection = null;
//Peek at the next recording but don't remove it from the queue yet, so we can inspect the start time for
//age limits and/or delay elapsed
@@ -381,17 +364,17 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
{
byte[] audio = Files.readAllBytes(nextRecording.getPath());
if(audio != null && audio.length > 0)
if(audio.length > 0)
{
mInputStream = new ByteArrayInputStream(audio);
mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS -
(nextRecording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);
while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
switch(mBroadcastFormat)
{
mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS;
case MP3:
mInputFrames = MP3FrameTools.split(audio);
break;
default:
throw new IllegalArgumentException("Unsupported broadcast format [" + mBroadcastFormat + "]");
}
mInputIdentifierCollection = nextRecording.getIdentifierCollection();
if(connected())
{
@@ -407,7 +390,8 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] error reading temporary audio " +
"stream recording [" + nextRecording.getPath().toString() + "] - skipping recording - ", ioe);
mInputStream = null;
mInputFrames = null;
mInputIdentifierCollection = null;
metadataUpdateRequired = false;
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;

View File

@@ -30,10 +30,8 @@ import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1AudioBroadcast
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2AudioStreamingBroadcaster;
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
import io.github.dsheirer.audio.convert.IAudioConverter;
import io.github.dsheirer.audio.convert.ISilenceGenerator;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3AudioConverter;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.audio.convert.MP3SilenceGenerator;
import io.github.dsheirer.preference.UserPreferences;
@@ -88,26 +86,6 @@ public class BroadcastFactory
return null;
}
/**
* Creates an audio convert to convert from 8 kHz PCM audio to the specified format
*
* @param configuration containing the requested output audio format
* @return audio convert or null
*/
public static IAudioConverter getAudioConverter(BroadcastConfiguration configuration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting)
{
switch(configuration.getBroadcastFormat())
{
case MP3:
return new MP3AudioConverter(inputAudioFormat, mp3Setting);
default:
mLog.info("Unrecognized broadcastAudio format: " + configuration.getBroadcastFormat().name());
}
return null;
}
/**
* Creates a broadcastAudio configuration for the specified server type and format
*

View File

@@ -26,13 +26,6 @@ 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 javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
@@ -48,6 +41,12 @@ import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.table.AbstractTableModel;
public class BroadcastModel extends AbstractTableModel implements Listener<AudioRecording>
{
@@ -58,7 +57,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
public static final int COLUMN_SERVER_ICON = 0;
public static final int COLUMN_BROADCAST_SERVER_TYPE = 0;
public static final int COLUMN_STREAM_NAME = 1;
public static final int COLUMN_BROADCASTER_STATUS = 2;
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
@@ -67,7 +66,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 6;
public static final String[] COLUMN_NAMES = new String[]
{"Streaming", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
@@ -575,15 +574,8 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
{
switch(columnIndex)
{
case COLUMN_SERVER_ICON:
String iconPath = configuredBroadcast.getBroadcastConfiguration()
.getBroadcastServerType().getIconPath();
if(iconPath != null && mIconModel != null)
{
return mIconModel.getScaledIcon(iconPath, 14);
}
break;
case COLUMN_BROADCAST_SERVER_TYPE:
return configuredBroadcast.getBroadcastServerType();
case COLUMN_STREAM_NAME:
return configuredBroadcast.getBroadcastConfiguration().getName();
case COLUMN_BROADCASTER_STATUS:
@@ -653,8 +645,8 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
case COLUMN_BROADCASTER_STREAMED_COUNT:
case COLUMN_BROADCASTER_ERROR_COUNT:
return Integer.class;
case COLUMN_SERVER_ICON:
return ImageIcon.class;
case COLUMN_BROADCAST_SERVER_TYPE:
return BroadcastServerType.class;
case COLUMN_STREAM_NAME:
default:
return String.class;

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 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
@@ -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.audio.broadcast;
public enum BroadcastServerType
@@ -23,17 +23,17 @@ public enum BroadcastServerType
/**
* Broadcastify feeds (ie streaming) service
*/
BROADCASTIFY("Broadcastify Feed", "/images/broadcastify.png"), //Icecast Server 2.3.2
BROADCASTIFY("Broadcastify Feed", "images/broadcastify.png"), //Icecast Server 2.3.2
/**
* Broadcastify calls - completed audio recording push service
*/
BROADCASTIFY_CALL("Broadcastify Call", "/images/broadcastify.png"),
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
ICECAST_HTTP("Icecast 2 (v2.4+)", "/images/icecast.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"),
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.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);
private String mLabel;

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 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
@@ -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.audio.broadcast;
public enum BroadcastState
@@ -103,10 +103,10 @@ public enum BroadcastState
private String mLabel;
private boolean mErrorState;
private BroadcastState(String label, boolean connected)
BroadcastState(String label, boolean errorState)
{
mLabel = label;
mErrorState = connected;
mErrorState = errorState;
}
public String toString()

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 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
@@ -15,23 +14,29 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.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 java.awt.Color;
import java.awt.Component;
import net.miginfocom.swing.MigLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.Color;
import java.awt.Component;
/**
* Table of broadcast streams and statuses.
*/
public class BroadcastStatusPanel extends JPanel
{
private JTable mTable;
@@ -41,6 +46,12 @@ public class BroadcastStatusPanel extends JPanel
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(BroadcastModel broadcastModel, UserPreferences userPreferences, String preferenceKey)
{
mBroadcastModel = broadcastModel;
@@ -65,6 +76,7 @@ public class BroadcastStatusPanel extends JPanel
renderer.setHorizontalAlignment(SwingConstants.CENTER);
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS).setCellRenderer(new StatusCellRenderer());
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCAST_SERVER_TYPE).setCellRenderer(new ServerTypeRenderer());
mColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, mPreferenceKey);
mScrollPane = new JScrollPane(mTable);
@@ -72,6 +84,37 @@ public class BroadcastStatusPanel extends JPanel
add(mScrollPane);
}
public class ServerTypeRenderer extends DefaultTableCellRenderer
{
public ServerTypeRenderer()
{
setOpaque(true);
setHorizontalAlignment(SwingConstants.CENTER);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
{
JLabel component = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if(value instanceof BroadcastServerType broadcastServerType)
{
component.setText(broadcastServerType.toString());
Icon icon = new Icon("empty", broadcastServerType.getIconPath());
ImageIcon imageIcon = icon.getIcon();
ImageIcon scaledIcon = IconModel.getScaledIcon(imageIcon, 13);
component.setIcon(scaledIcon);
}
else
{
component.setText(null);
component.setIcon(null);
}
return component;
}
}
/**
* Custom cell renderer for the broadcast state column.
*/

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;

View File

@@ -68,6 +68,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
private static final String MULTIPART_TYPE = "multipart";
private static final String DEFAULT_SUBTYPE = "form-data";
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
private ScheduledFuture<?> mBroadcastifyTestFuture;
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
private HttpClient mHttpClient = HttpClient.newBuilder()
@@ -109,6 +110,12 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
setBroadcastState(BroadcastState.ERROR);
}
if(mBroadcastifyTestFuture == null && getBroadcastConfiguration().isTestEnabled())
{
// Test periodically so we don't get marked offline due to radio inactivity
mBroadcastifyTestFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new BroadcastifyCallTest(), getBroadcastConfiguration().getTestInterval(), getBroadcastConfiguration().getTestInterval(), TimeUnit.MINUTES);
}
if(mAudioRecordingProcessorFuture == null)
{
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
@@ -122,6 +129,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
@Override
public void stop()
{
if(mBroadcastifyTestFuture != null)
{
mBroadcastifyTestFuture.cancel(true);
mBroadcastifyTestFuture = null;
}
if(mAudioRecordingProcessorFuture != null)
{
mAudioRecordingProcessorFuture.cancel(true);
@@ -131,6 +143,23 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
}
}
public class BroadcastifyCallTest implements Runnable
{
@Override
public void run()
{
String response = testConnection(getBroadcastConfiguration());
if(response != null && response.toLowerCase().startsWith("ok"))
{
mLog.info("Broadcastify Calls keep-alive success");
}
else
{
mLog.info("Broadcastify Calls keep-alive failure");
}
}
}
/**
* Prepares for disposal
*/
@@ -418,28 +447,20 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
*/
private static String getTo(AudioRecording audioRecording)
{
List<Identifier> toIdentifiers = audioRecording.getIdentifierCollection().getIdentifiers(Role.TO);
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
//Format a patch group first, if there are multiples
for(Identifier identifier: toIdentifiers)
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return format(patchGroupIdentifier);
}
return format(patchGroupIdentifier);
}
for(Identifier identifier: toIdentifiers)
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
}
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
}
return "0";

View File

@@ -24,7 +24,9 @@ import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -42,6 +44,8 @@ public class BroadcastifyCallConfiguration extends BroadcastConfiguration
private IntegerProperty mSystemID = new SimpleIntegerProperty();
private StringProperty mApiKey = new SimpleStringProperty();
private BooleanProperty mTestEnabled= new SimpleBooleanProperty(false);
private IntegerProperty mTestInterval = new SimpleIntegerProperty(15);
/**
* Constructor for faster jackson
@@ -128,6 +132,34 @@ public class BroadcastifyCallConfiguration extends BroadcastConfiguration
return BroadcastServerType.BROADCASTIFY_CALL;
}
/**
* Indicates if this instance should schedule tests after connecting
*/
@JacksonXmlProperty(isAttribute = true, localName = "test_enabled")
public boolean isTestEnabled()
{
return mTestEnabled.get();
}
public void setTestEnabled(boolean enabled)
{
mTestEnabled.set(enabled);
}
/**
* Indicates the interval at which tests should be performed while enabled
*/
@JacksonXmlProperty(isAttribute = true, localName = "test_interval")
public int getTestInterval()
{
return mTestInterval.get();
}
public void setTestInterval(int interval)
{
mTestInterval.set(interval);
}
@Override
public BroadcastConfiguration copyOf()
{

View File

@@ -54,6 +54,7 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
setBitRate(16);
setChannels(1);
setSampleRate(8000);
setInline(true);
}
public static BroadcastifyFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
@@ -86,9 +87,9 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setInline(getInline());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);
//Icecast Configuration Parameters

View File

@@ -28,7 +28,7 @@ import io.github.dsheirer.audio.convert.MP3Setting;
public abstract class IcecastAudioBroadcaster extends AudioStreamingBroadcaster
{
private IBroadcastMetadataUpdater mMetadataUpdater;
private AliasModel mAliasModel;
protected AliasModel mAliasModel;
public IcecastAudioBroadcaster(BroadcastConfiguration broadcastConfiguration, InputAudioFormat inputAudioFormat,
MP3Setting mp3Setting, AliasModel aliasModel)

View File

@@ -23,6 +23,8 @@ 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;
@@ -73,6 +75,12 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
*/
public void update(IdentifierCollection identifierCollection)
{
if(mIcecastConfiguration.hasInline())
{
// Never send out-of-band metadata if inline metadata is enabled.
return;
}
StringBuilder sb = new StringBuilder();
try
@@ -84,7 +92,7 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
sb.append("/admin/metadata?mode=updinfo&mount=");
sb.append(URLEncoder.encode(mIcecastConfiguration.getMountPoint(), UTF8));
sb.append("&charset=UTF%2d8");
sb.append("&song=").append(URLEncoder.encode(getSong(identifierCollection), UTF8));
sb.append("&song=").append(URLEncoder.encode(IcecastMetadata.getTitle(identifierCollection, mAliasModel), UTF8));
}
catch(UnsupportedEncodingException uee)
{
@@ -157,82 +165,4 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
}
}
/**
* Creates the song information for a metadata update
*/
private String getSong(IdentifierCollection identifierCollection)
{
StringBuilder sb = new StringBuilder();
if(identifierCollection != null)
{
AliasList aliasList = mAliasModel.getAliasList(identifierCollection);
Identifier to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.PATCH_GROUP, Role.TO);
if(to == null)
{
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
}
if(to == null)
{
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
if(!toIdentifiers.isEmpty())
{
to = toIdentifiers.get(0);
}
}
if(to != null)
{
sb.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
if(aliases != null && !aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
else
{
sb.append("TO:UNKNOWN");
}
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
if(from == null)
{
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
if(!fromIdentifiers.isEmpty())
{
from = fromIdentifiers.get(0);
}
}
if(from != null)
{
sb.append(" FROM:").append(from);
List<Alias> aliases = aliasList.getAliases(from);
if(aliases != null && !aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
else
{
sb.append(" FROM:UNKNOWN");
}
}
else
{
sb.append("Scanning ....");
}
return sb.toString();
}
}

View File

@@ -45,6 +45,7 @@ public abstract class IcecastConfiguration extends BroadcastConfiguration
private int mChannels = 1;
private int mSampleRate = 8000;
private String mURL;
private boolean mInline = true;
public IcecastConfiguration(BroadcastFormat format)
{
@@ -275,6 +276,37 @@ public abstract class IcecastConfiguration extends BroadcastConfiguration
return mURL != null;
}
/**
* Control whether metadata is sent inline or out-of-band.
*/
@JacksonXmlProperty(isAttribute = true, localName = "inline")
public boolean getInline()
{
return mInline;
}
@JsonIgnore
public int getInlineInterval()
{
if (hasBitRate())
{
// Interval = BitRate * 1000 / 8 = 1 second
return getBitRate() * 125;
}
return -1;
}
public void setInline(boolean inline)
{
mInline = inline;
}
public boolean hasInline()
{
// Bitrate must be known to calculate metadata interval
return mInline != false && hasBitRate();
}
@Override
public boolean isValid()
{

View File

@@ -23,8 +23,17 @@ import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3AudioConverter;
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 java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.future.ConnectFuture;
@@ -40,12 +49,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
@@ -80,14 +83,41 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
protected void broadcastAudio(byte[] audio, IdentifierCollection identifierCollection)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
IoBuffer buffer = IoBuffer.allocate(audio.length);
buffer.put(audio);
buffer.flip();
if(mInlineActive)
{
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
buffer.setAutoExpand(true);
if (mInlineRemaining == -1)
{
mInlineRemaining = mInlineInterval;
}
int audioOffset = 0;
while(audioOffset < audio.length)
{
byte[] chunk = Arrays.copyOfRange(audio, audioOffset, Math.min(audioOffset + mInlineRemaining, audio.length));
mInlineRemaining -= chunk.length;
audioOffset += chunk.length;
buffer.put(chunk);
if(mInlineRemaining == 0)
{
mInlineRemaining = mInlineInterval;
buffer.put(metadata);
}
}
buffer.shrink();
}
else
{
buffer.put(audio);
}
buffer.flip();
mStreamingSession.write(buffer);
}
}
@@ -113,8 +143,9 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
// mSocketConnector.getFilterChain().addLast("logger",
// new LoggingFilter(IcecastHTTPAudioBroadcaster.class));
// LoggingFilter loggingFilter = new LoggingFilter(IcecastHTTPAudioBroadcaster.class);
// loggingFilter.setMessageReceivedLogLevel(LogLevel.DEBUG);
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec());
mSocketConnector.setHandler(new IcecastHTTPIOHandler());
@@ -238,6 +269,17 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
mHTTPHeaders.put(IcecastHeader.BITRATE.getValue(), String.valueOf(getConfiguration().getBitRate()));
}
if(getConfiguration().hasInline())
{
mInlineActive = true;
mInlineInterval = getConfiguration().getInlineInterval();
mHTTPHeaders.put(IcecastHeader.METAINT.getValue(), String.valueOf(mInlineInterval));
}
else
{
mInlineActive = false;
}
}
return mHTTPHeaders;
@@ -274,10 +316,49 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
* intercept the out of bounds exception and then inspect the message hex dump to see if it's an
* OK response and then play like everything is well and good.
*/
if(cause instanceof ArrayIndexOutOfBoundsException &&
((ProtocolDecoderException)throwable).getHexdump().startsWith(HTTP_1_0_OK_HEX_DUMP))
if(cause instanceof ArrayIndexOutOfBoundsException)
{
setBroadcastState(BroadcastState.CONNECTED);
String hexDump = ((ProtocolDecoderException)throwable).getHexdump();
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
{
setBroadcastState(BroadcastState.CONNECTED);
}
else
{
HttpDumpMessage message = new HttpDumpMessage(hexDump);
if(message.hasHttpResponseCode())
{
switch(message.getHttpResponseCode())
{
case 403: //Forbidden
if(message.toString().contains("Mountpoint in use"))
{
mLog.error("Stream [" + getStreamName() + "] - unable to connect - mountpoint in use");
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
disconnect();
}
else
{
mLog.error("String [" + getStreamName() + "] - HTTP 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();
}
}
else
{
mLog.error("HTTP protocol decoder error - message:\n\n" + message);
setBroadcastState(BroadcastState.DISCONNECTED);
disconnect();
}
}
}
else
{
@@ -333,4 +414,97 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
}
}
}
/**
* Class for parsing HTTP response messages from Icecast 2.4.2+
*/
public class HttpDumpMessage
{
private String mHexDump;
private String mMessage;
private int mHttpResponseCode = -1;
public HttpDumpMessage(String hexDump)
{
mHexDump = hexDump;
String[] split = mHexDump.split(" ");
byte[] bytes = new byte[split.length];
int pointer = 0;
for(String a : split)
{
try
{
int value = Integer.parseInt(a, 16);
bytes[pointer++] = (byte) (value & 0xFF);
}
catch(Exception e)
{
pointer++;
}
}
mMessage = new String(bytes);
Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
Matcher m = pattern.matcher(mMessage);
if(m.find())
{
try
{
mHttpResponseCode = Integer.parseInt(m.group(1));
}
catch(Exception e)
{
mLog.error("Unable to parse HTTP response code that was matched from message: " + m.group(1));
}
}
}
/**
* Indicates if an HTTP response code was parsed from the message
* @return true if it was parsed.
*/
public boolean hasHttpResponseCode()
{
return mHttpResponseCode != -1;
}
/**
* Response code parsed from the hexdump message
* @return value.
*/
public int getHttpResponseCode()
{
return mHttpResponseCode;
}
@Override
public String toString()
{
return mMessage;
}
}
public static void main(String[] args)
{
String text = "HTTP/1.0 403 Forbidden";
Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
Matcher m = pattern.matcher(text);
if(m.find())
{
mLog.info("Matching Group Count: " + m.groupCount());
mLog.info("Match 0: " + m.group(0));
mLog.info("Match 1: " + m.group(1));
}
else
{
mLog.info("No Match");
}
}
}

View File

@@ -55,6 +55,7 @@ public class IcecastHTTPConfiguration extends IcecastConfiguration
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setInline(getInline());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);

View File

@@ -33,6 +33,7 @@ public enum IcecastHeader
PUBLIC("ice-public"),
URL("ice-url"),
USER_AGENT("User-Agent"),
METAINT("icy-metaint"),
UNKNOWN("unknown");

View File

@@ -0,0 +1,138 @@
/*
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
*
*
*/
package io.github.dsheirer.audio.broadcast.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.icecast.IcecastConfiguration;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class IcecastMetadata
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadata.class);
/**
* Creates the title for a metadata update
*/
public static String getTitle(IdentifierCollection identifierCollection, AliasModel AliasModel)
{
StringBuilder sb = new StringBuilder();
if(identifierCollection != null)
{
AliasList aliasList = AliasModel.getAliasList(identifierCollection);
Identifier to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.PATCH_GROUP, Role.TO);
if(to == null)
{
to = identifierCollection.getIdentifier(IdentifierClass.USER, Form.TALKGROUP, Role.TO);
}
if(to == null)
{
List<Identifier> toIdentifiers = identifierCollection.getIdentifiers(Role.TO);
if(!toIdentifiers.isEmpty())
{
to = toIdentifiers.get(0);
}
}
if(to != null)
{
sb.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
if(aliases != null && !aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
else
{
sb.append("TO:UNKNOWN");
}
Identifier from = identifierCollection.getIdentifier(IdentifierClass.USER, Form.RADIO, Role.FROM);
if(from == null)
{
List<Identifier> fromIdentifiers = identifierCollection.getIdentifiers(Role.FROM);
if(!fromIdentifiers.isEmpty())
{
from = fromIdentifiers.get(0);
}
}
if(from != null)
{
sb.append(" FROM:").append(from);
List<Alias> aliases = aliasList.getAliases(from);
if(aliases != null && !aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
else
{
sb.append(" FROM:UNKNOWN");
}
}
else
{
sb.append("Scanning...");
}
return sb.toString();
}
public static String formatInline(String title)
{
title = "StreamTitle='" + title + "';";
int chunks = (int)Math.ceil((title.length() + 1) / 16.0d);
int nulls = (chunks * 16) - title.length();
char[] padding = new char[nulls];
StringBuilder sb = new StringBuilder();
sb.append((char)chunks).append(title).append(padding);
return sb.toString();
}
}

View File

@@ -24,8 +24,16 @@ import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3AudioConverter;
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 java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.channels.UnresolvedAddressException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
@@ -35,17 +43,10 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.channels.UnresolvedAddressException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastTCPAudioBroadcaster.class);
private final static String TERMINATOR = "\r\n";
private final static String TERMINATOR = "\n";
private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds
@@ -78,11 +79,35 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
protected void broadcastAudio(byte[] audio, IdentifierCollection identifierCollection)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{
mStreamingSession.write(audio);
if(mInlineActive)
{
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
if (mInlineRemaining == -1)
{
mInlineRemaining = mInlineInterval;
}
int audioOffset = 0;
while(audioOffset < audio.length)
{
byte[] chunk = Arrays.copyOfRange(audio, audioOffset, Math.min(audioOffset + mInlineRemaining, audio.length));
mInlineRemaining -= chunk.length;
audioOffset += chunk.length;
mStreamingSession.write(chunk);
if(mInlineRemaining == 0)
{
mInlineRemaining = mInlineInterval;
mStreamingSession.write(metadata);
}
}
}
else
{
mStreamingSession.write(audio);
}
}
}
@@ -188,7 +213,13 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
}
else
{
setBroadcastState(BroadcastState.DISCONNECTED);
//Only set broadcast state to disconnected if it's not already in an error state, to prevent a restart. We
//want to preserve the error state that got us here, so the user can see it.
if(!getBroadcastState().isErrorState())
{
setBroadcastState(BroadcastState.DISCONNECTED);
}
mLastConnectionAttempt = System.currentTimeMillis();
}
}
@@ -238,7 +269,18 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
.append(getConfiguration().getDescription()).append(TERMINATOR);
}
sb.append(TERMINATOR).append(TERMINATOR);
if(getConfiguration().hasInline())
{
mInlineActive = true;
mInlineInterval = getConfiguration().getInlineInterval();
sb.append(IcecastHeader.METAINT.getValue()).append(SEPARATOR)
.append(String.valueOf(mInlineInterval))
.append(TERMINATOR);
}
else
{
mInlineActive = false;
}
session.write(sb.toString());
}

View File

@@ -63,6 +63,7 @@ public class IcecastTCPConfiguration extends IcecastConfiguration
copy.setName(getName());
copy.setHost(getHost());
copy.setPort(getPort());
copy.setInline(getInline());
copy.setPassword(getPassword());
copy.setDelay(getDelay());
copy.setEnabled(false);

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 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
@@ -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.audio.broadcast.icecast.codec;
import org.apache.mina.core.buffer.IoBuffer;

View File

@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
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 org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.buffer.IoBuffer;
@@ -93,7 +94,7 @@ public class ShoutcastV1AudioBroadcaster extends AudioStreamingBroadcaster
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
protected void broadcastAudio(byte[] audio, IdentifierCollection identifierCollection)
{
if(audio != null && audio.length > 0 && connect() && mStreamingSession != null && mStreamingSession.isConnected())
{

View File

@@ -109,7 +109,7 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas
* Broadcasts the audio frame or sequence
*/
@Override
protected void broadcastAudio(byte[] audio)
protected void broadcastAudio(byte[] audio, IdentifierCollection identifierCollection)
{
//Dispatch any queued metadata messages
UltravoxMessage metadataMessage = mMetadataMessageQueue.poll();

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 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
@@ -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.audio.broadcast.shoutcast.v2.ultravox;
import io.github.dsheirer.audio.broadcast.BroadcastFormat;

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 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
@@ -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.audio.broadcast.shoutcast.v2.ultravox;
import io.github.dsheirer.bits.BinaryMessage;

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 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
@@ -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.audio.broadcast.shoutcast.v2.ultravox;
public class UltravoxMessageFactory

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2021 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

View File

@@ -0,0 +1,133 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.convert;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
public abstract class AudioFrames
{
protected final int audioDuration;
protected final List<byte[]> audioFrames;
protected int mCurrentFrame = -1;
/**
* Create new AudioFrame
* @param audioDuration total duration in milliseconds
* @param audioFrames series of audio frames, split on frame boundaries
*/
public AudioFrames(int audioDuration, List<byte[]> audioFrames)
{
this.audioDuration = audioDuration;
this.audioFrames = audioFrames;
}
/**
* Get total duration of frames
* @return total duration in milliseconds
*/
public int getDuration()
{
return audioDuration;
}
/**
* Get all frames
* @return list of byte arrays, with each element representing a single frame of audio
*/
public List<byte[]> getFrames()
{
return audioFrames;
}
/**
* Get current frame
* @return byte array with the current frame of audio
*/
public byte[] getCurrentFrame()
{
return audioFrames.get(mCurrentFrame);
}
/**
* Get current frame duration
* @return frame duration in milliseconds
*/
public abstract int getCurrentFrameDuration();
/**
* Indicates if there is a frame before the current one
* @return true if the frame exists, otherwise false
*/
public boolean hasPrevFrame()
{
int prevFrame = mCurrentFrame - 1;
return prevFrame >= 0 && prevFrame < audioFrames.size();
}
/**
* Indicates if there is a frame after the current one
* @return true if the frame exists, otherwise false
*/
public boolean hasNextFrame()
{
int nextFrame = mCurrentFrame + 1;
return nextFrame >= 0 && nextFrame < audioFrames.size();
}
/**
* Seek to the previous frame
*/
public void prevFrame()
{
mCurrentFrame -= 1;
}
/**
* Seek to the next frame
*/
public void nextFrame()
{
mCurrentFrame += 1;
}
/**
* Restart from beginning
* Must call nextFrame() to get first frame
*/
public void restart()
{
mCurrentFrame = -1;
}
/**
* Seek by the specified time
* @param duration_ms time in milliseconds, positive or negative
*/
public void seek(int duration_ms)
{
int step = duration_ms < 0 ? -1 : 1;
int actual_ms = 0;
while(FastMath.abs(actual_ms) < FastMath.abs(duration_ms))
{
mCurrentFrame += step;
actual_ms += getCurrentFrameDuration();
}
}
}

View File

@@ -22,5 +22,5 @@ import java.util.List;
public interface ISilenceGenerator
{
List<byte[]> generate(long duration);
AudioFrames generate(long duration);
}

View File

@@ -20,6 +20,7 @@
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioFormats;
import java.util.EnumSet;
import javax.sound.sampled.AudioFormat;
@@ -29,8 +30,8 @@ import javax.sound.sampled.AudioFormat;
public enum InputAudioFormat
{
SR_8000(AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO, "16-Bit 8000 Hz (no resample)"),
SR_16000(AudioFormats.PCM_SIGNED_16000_HZ_16_BIT_MONO, "16-Bit 16000 Hz"),
SR_22050(AudioFormats.PCM_SIGNED_22050_HZ_16_BIT_MONO, "16-Bit 22050 Hz (default)"),
SR_16000(AudioFormats.PCM_SIGNED_16000_HZ_16_BIT_MONO, "16-Bit 16000 Hz (default)"),
SR_22050(AudioFormats.PCM_SIGNED_22050_HZ_16_BIT_MONO, "16-Bit 22050 Hz"),
SR_44100(AudioFormats.PCM_SIGNED_44100_HZ_16_BIT_MONO, "16-Bit 44100 Hz"),
SR_32_8000(AudioFormats.PCM_SIGNED_8000_HZ_32_BIT_MONO, "32-Bit 8000 Hz (no resample)"),
@@ -41,6 +42,17 @@ public enum InputAudioFormat
private AudioFormat mAudioFormat;
private String mLabel;
/**
* Set that includes sample rates of 8 or 16 kHz
*/
public static final EnumSet<InputAudioFormat> SAMPLE_RATES_8_16 = EnumSet.of(SR_8000, SR_32_8000, SR_16000, SR_32_16000);
/**
* Set that includes sample rates of 8, 16, or 22 kHz
*/
public static final EnumSet<InputAudioFormat> SAMPLE_RATES_8_16_22 = EnumSet.of(SR_8000, SR_32_8000, SR_16000,
SR_32_16000, SR_22050, SR_32_22050);
/**
* Constructs an instance
* @param audioFormat for the specified sample rate
@@ -56,7 +68,7 @@ public enum InputAudioFormat
*/
public static InputAudioFormat getDefault()
{
return SR_22050;
return SR_16000;
}
/**

View File

@@ -18,19 +18,19 @@
*/
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioUtils;
import io.github.dsheirer.dsp.filter.resample.RealResampler;
import io.github.dsheirer.sample.ConversionUtils;
import net.sourceforge.lame.lowlevel.LameEncoder;
import net.sourceforge.lame.mp3.Lame;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import net.sourceforge.lame.lowlevel.LameEncoder;
import net.sourceforge.lame.mp3.Lame;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Converts PCM audio packets to Mono, MP3 encoded audio
@@ -45,19 +45,32 @@ public class MP3AudioConverter implements IAudioConverter
private byte[] mOutputFramesBuffer;
private InputAudioFormat mInputAudioFormat;
private boolean mNormalizeAudio;
/**
* Constructs an instance.
*
* @param inputAudioFormat for the desired input sample rate and bit size (resampled from default 8 kHz as needed)
* @param setting to configure the LAME encoder
* @param normalizeAudio to normalize the audio gain prior to encoding
*/
public MP3AudioConverter(InputAudioFormat inputAudioFormat, MP3Setting setting)
public MP3AudioConverter(InputAudioFormat inputAudioFormat, MP3Setting setting, boolean normalizeAudio)
{
mInputAudioFormat = inputAudioFormat;
mEncoder = LameFactory.getLameEncoder(inputAudioFormat, setting);
mNormalizeAudio = normalizeAudio;
//Ensure input audio sample rate is supported for the MP3 setting and update as necessary - should never happen.
if((mInputAudioFormat.getSampleRate() - mEncoder.getEffectiveSampleRate()) > 1.0)
{
mInputAudioFormat = InputAudioFormat.getDefault();
mLog.warn("MP3 setting [" + setting + "] does not support requested input audio sample rate [" +
inputAudioFormat + "] - using default sample rate [" + mInputAudioFormat + "]");
mEncoder = LameFactory.getLameEncoder(inputAudioFormat, setting);
}
//Resampling is only required if desired input sample rate is not system default of 8kHz
if(inputAudioFormat != InputAudioFormat.SR_8000 && inputAudioFormat != InputAudioFormat.SR_32_8000)
if(mInputAudioFormat != InputAudioFormat.SR_8000 && mInputAudioFormat != InputAudioFormat.SR_32_8000)
{
mResampler = LameFactory.getResampler(inputAudioFormat);
}
@@ -74,6 +87,11 @@ public class MP3AudioConverter implements IAudioConverter
{
List<byte[]> converted = new ArrayList<>();
if(mNormalizeAudio)
{
audioPackets = AudioUtils.normalize(audioPackets);
}
if(mResampler != null)
{
audioPackets = mResampler.resample(audioPackets);

View File

@@ -17,35 +17,30 @@
* ****************************************************************************
*/
package io.github.dsheirer.source.tuner.usb.converter;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
package io.github.dsheirer.audio.convert;
import java.util.List;
/**
* Base class for native samples byte buffer. This class holds a copy of the byte array of samples
* that was copied from a native byte buffer into the JVM heap space.
*
* Consumers of this sample buffer must make a copy of the contents in either interleaved or non-interleaved
* sample array buffers.
*/
public abstract class NativeSampleBuffer
public class MP3AudioFrames extends AudioFrames
{
private byte[] mSamples;
public NativeSampleBuffer(byte[] samples)
/**
* Create new MP3AudioFrame
* @param audioDuration total duration in milliseconds
* @param audioFrames series of audio frames, split on frame boundaries
*/
public MP3AudioFrames(int audioDuration, List<byte[]> audioFrames)
{
mSamples = samples;
super(audioDuration, audioFrames);
}
protected byte[] getSamples()
/**
* Get current frame duration
* @return frame duration in milliseconds
*/
public int getCurrentFrameDuration()
{
return mSamples;
return MP3Header.getFrameDuration(getCurrentFrame(), 0);
}
public abstract List<ComplexSamples> convert();
public abstract List<InterleavedComplexSamples> convertInterleaved();
}

View File

@@ -55,6 +55,7 @@ public class MP3FrameInspector
}
}
/* TODO: Rework for MP3SilenceGenerator returning IAudioFrames
public static void main(String[] args)
{
mLog.info("Starting ...");
@@ -66,4 +67,5 @@ public class MP3FrameInspector
mLog.info("Finished");
}
*/
}

View File

@@ -0,0 +1,59 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.convert;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
public class MP3FrameTools {
private MP3FrameTools() {}
/**
* Split MP3 audio at frame boundaries
* @param input byte array of audio
* @return list of byte arrays containing audio frames
*/
public static MP3AudioFrames split(byte[] input)
{
List<byte[]> mp3Frames = new ArrayList<>();
int audioDuration = 0;
int offset = 0;
while(offset < input.length)
{
if(MP3Header.hasSync(input, offset))
{
int framelen = MP3Header.getFrameLength(input, offset);
mp3Frames.add(Arrays.copyOfRange(input, offset, offset + FastMath.min(framelen, input.length - offset)));
audioDuration += MP3Header.getFrameDuration(input, offset);
offset += framelen;
}
else
{
offset += 1;
}
}
return new MP3AudioFrames(audioDuration, mp3Frames);
}
}

View File

@@ -19,6 +19,10 @@
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.convert.MPEGLayer;
import io.github.dsheirer.audio.convert.MPEGVersion;
import org.apache.commons.math3.util.FastMath;
/**
* Utility class for inspecting/parsing MP3 frames
*/
@@ -88,6 +92,71 @@ public class MP3Header
return ChannelMode.fromValue((frame[offset + 3] & 0xC0) >> 6);
}
/**
* Indicates if the frame has padding
* @param frame
* @param offset
* @return boolean
*/
public static boolean isPadded(byte[] frame, int offset)
{
return (frame[offset + 2] & 0x02) == 0x02;
}
/**
* Get the number of bytes in the frame
* @param frame
* @param offset
* @return frame length in bytes
*/
public static int getFrameLength(byte[] frame, int offset)
{
return (getFrameSamples(frame, offset) / 8) * (getBitRate(frame, offset) * 1000) / getSampleRate(frame, offset) + (isPadded(frame, offset) ? 1 : 0);
}
/**
* Get the number of samples in the frame
* @param frame
* @param offset
* @return length of frame in samples
*/
public static int getFrameSamples(byte[] frame, int offset)
{
switch(getMPEGVersion(frame, offset))
{
case V_1:
switch(getMPEGLayer(frame, offset))
{
case LAYER1: return 384;
case LAYER2: return 1152;
case LAYER3: return 1152;
default:
}
case V_2:
case V_2_5:
switch(getMPEGLayer(frame, offset))
{
case LAYER1: return 384;
case LAYER2: return 1152;
case LAYER3: return 576;
default:
}
default:
}
return 0;
}
/**
* Get the frame duration in milliseconds
* @param frame
* @param offset
* @return duration in milliseconds
*/
public static int getFrameDuration(byte[] frame, int offset)
{
return (int)FastMath.round((double)getFrameSamples(frame, offset) / (double)getSampleRate(frame, offset) * 1000);
}
/**
* Inspects the frame bytes and provides details about the header.
*/

View File

@@ -19,6 +19,8 @@
package io.github.dsheirer.audio.convert;
import java.util.Arrays;
import java.util.List;
import net.sourceforge.lame.mp3.Lame;
/**
@@ -34,8 +36,8 @@ public enum MP3Setting
CBR_16(false, 16, "CBR - Constant Bit Rate 16 kbps (default)"),
CBR_32(false, 32, "CBR - Constant Bit Rate 32 kbps"),
ABR_56(true, 56, "ABR - Average Bit Rate 56 kbps"),
VBR_5(true, Lame.QUALITY_MIDDLE, "VBR - Middle Quality (5)"),
VBR_7(true, Lame.QUALITY_LOW, "VBR - Low Quality (7)");
VBR_5(true, Lame.QUALITY_MIDDLE, "VBR - Variable Bit Rate - Better Quality"),
VBR_7(true, Lame.QUALITY_LOW, "VBR - Variable Bit Rate - Good Quality");
private boolean mVariableBitRate;
private int mSetting;
@@ -79,6 +81,30 @@ public enum MP3Setting
return mSetting;
}
/**
* Input audio formats that are supported by the MP3Setting value.
* @return list of supported input audio sample rates.
*/
public List<InputAudioFormat> getSupportedSampleRates()
{
switch(this)
{
case CBR_16:
{
return InputAudioFormat.SAMPLE_RATES_8_16.stream().toList();
}
case CBR_32:
case VBR_7:
{
return InputAudioFormat.SAMPLE_RATES_8_16_22.stream().toList();
}
default:
{
return Arrays.stream(InputAudioFormat.values()).toList();
}
}
}
@Override
public String toString()
{

View File

@@ -18,21 +18,21 @@
*/
package io.github.dsheirer.audio.convert;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
public class MP3SilenceGenerator implements ISilenceGenerator
{
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
public static final int MP3_BIT_RATE = 16;
public static final boolean CONSTANT_BIT_RATE = false;
private MP3AudioConverter mMP3AudioConverter;
private InputAudioFormat mInputAudioFormat;
private byte[] mPreviousPartialFrameData;
private byte[] mSilenceFrame;
private int mSilenceFrameDuration;
/**
* Generates MP3 audio silence frames
@@ -40,20 +40,18 @@ public class MP3SilenceGenerator implements ISilenceGenerator
public MP3SilenceGenerator(InputAudioFormat inputAudioFormat, MP3Setting setting)
{
mInputAudioFormat = inputAudioFormat;
mMP3AudioConverter = new MP3AudioConverter(inputAudioFormat, setting);
mMP3AudioConverter = new MP3AudioConverter(inputAudioFormat, setting, false);
generate_frame();
}
/**
* Generates silence frames
* @param duration_ms in milliseconds
* Generates a single silence frame
* @return
*/
public List<byte[]> generate(long duration_ms)
private void generate_frame()
{
double duration_secs = (double)duration_ms / 1000.0;
//We generate silence at 8000 kHz, because it gets resampled by the silence generator to the target rate
int length = (int)(duration_secs * InputAudioFormat.SR_8000.getSampleRate());
int length = (int)(InputAudioFormat.SR_8000.getSampleRate());
List<float[]> silenceBuffers = new ArrayList<>();
@@ -65,7 +63,40 @@ public class MP3SilenceGenerator implements ISilenceGenerator
added += chunk;
}
return mMP3AudioConverter.convert(silenceBuffers);
List<byte[]> mp3Audio = mMP3AudioConverter.convert(silenceBuffers);
byte[] silence = new byte[0];
for(byte[] mp3Chunk: mp3Audio)
{
silence = merge(silence, mp3Chunk);
}
MP3AudioFrames mp3Frames = MP3FrameTools.split(silence);
if(mp3Frames.hasNextFrame())
{
mp3Frames.nextFrame();
mSilenceFrame = mp3Frames.getCurrentFrame();
mSilenceFrameDuration = mp3Frames.getCurrentFrameDuration();
}
}
/**
* Generates silence frames
* @param duration_ms in milliseconds
* @return
*/
public MP3AudioFrames generate(long duration_ms)
{
List<byte[]> silenceFrames = new ArrayList<>();
int silenceDuration = 0;
while(silenceDuration < duration_ms)
{
silenceFrames.add(mSilenceFrame);
silenceDuration += mSilenceFrameDuration;
}
return new MP3AudioFrames(silenceDuration, silenceFrames);
}
private static byte[] merge(byte[] a, byte[] b)
@@ -90,6 +121,7 @@ public class MP3SilenceGenerator implements ISilenceGenerator
return c;
}
/* TODO: Rework for MP3SilenceGenerator returning IAudioFrames
public static void main(String[] args)
{
mLog.debug("Starting ...");
@@ -114,4 +146,5 @@ public class MP3SilenceGenerator implements ISilenceGenerator
mLog.debug("Finished");
}
*/
}

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 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
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.audio.playback;
@@ -30,7 +29,6 @@ import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
@@ -69,7 +67,6 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
private IconModel mIconModel;
private SettingsManager mSettingsManager;
private SourceManager mSourceManager;
private IAudioController mController;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
@@ -78,11 +75,10 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
private AudioChannelsPanel mAudioChannelsPanel;
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
SourceManager sourceManager, IAudioController controller, AliasModel aliasModel)
IAudioController controller, AliasModel aliasModel)
{
mIconModel = iconModel;
mSettingsManager = settingsManager;
mSourceManager = sourceManager;
mController = controller;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;

View File

@@ -1,41 +1,44 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.bits;
import io.github.dsheirer.edac.CRC;
import java.io.UnsupportedEncodingException;
import java.util.BitSet;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
import java.util.BitSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BinaryMessage extends BitSet
{
private static final long serialVersionUID = 1L;
private static final Logger LOGGER = LoggerFactory.getLogger(BinaryMessage.class);
private static final int[] CHARACTER_7_BIT = new int[]{0, 1, 2, 3, 4, 5, 6};
private static final int[] CHARACTER_8_BIT = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
private static final String UTF_8 = "UTF-8";
/**
* Logical (ie constructed) size of this bitset, despite the actual size of
* the super bitset that this class is based on
*/
private int mSize = 0;
private int mSize;
/**
* Pointer to the next fill index location, when adding bits to this bitset
@@ -43,21 +46,21 @@ public class BinaryMessage extends BitSet
*/
private int mPointer = 0;
/**
* Bitset that buffers bits added one at a time, up to the size of the this
* bitset.
*
* Note: the super class bitset behind this class may have a size larger
* that the size parameter specified.
* @param size
*/
/**
* Used for temporary storage of CRC check results when we're passing this
* message to an EDAC function.
*/
private CRC mCRC;
/**
* Constructs a bitset that buffers bits added one at a time, up to the size
* of the this bitset.
* <p>
* Note: the super class bitset behind this class may have a size larger
* that the size parameter specified.
*
* @param size of constructed bitset
*/
public BinaryMessage(int size)
{
super(size);
@@ -214,9 +217,7 @@ public class BinaryMessage extends BitSet
}
/**
* Returns a (new) copy of this bitsetbuffer
*
* @return
* @return a (new) copy of this binary message
*/
public BinaryMessage copy()
{
@@ -1040,6 +1041,51 @@ public class BinaryMessage extends BitSet
return buffer;
}
/**
* Loads the hexadecimal text into a binary message
* @param hex text to load from
* @return loaded binary message
*/
public static BinaryMessage loadHex(String hex)
{
if(!hex.matches("[0-9A-F]*"))
{
throw new IllegalArgumentException("Message must contain only 0-9 and A-F hexadecimal characters");
}
int length = hex.length() / 2;
if(length * 2 < hex.length())
{
length++;
}
BinaryMessage buffer = new BinaryMessage(length * 8);
int bufferOffset = 0;
for(int x = 0; x < hex.length(); x += 2)
{
int endIndex = x + 2;
if(endIndex > hex.length())
{
endIndex--;
}
String rawHex = hex.substring(x, endIndex);
if(rawHex.length() == 1)
{
rawHex = rawHex + "0";
}
int value = Integer.parseInt(rawHex, 16);
buffer.setByte(bufferOffset, (byte)(value & 0xFF));
bufferOffset += 8;
}
return buffer;
}
/**
* Left rotates the bits between start and end indices, number of places.
*/
@@ -1135,19 +1181,101 @@ public class BinaryMessage extends BitSet
this.xor(mask);
}
public static void main(String[] args)
/**
* Parse ISO-7 bit characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseISO7(int offset, int characterCount)
{
BinaryMessage message = new BinaryMessage(10);
message.set(1);
message.set(4);
message.set(5);
message.set(8);
message.set(9);
byte[] bytes = new byte[characterCount];
System.out.println("Hex: " + message.toHexString() + " Size:" + message.size());
for(int x = 0; x < 10; x++)
for(int x = 0; x < characterCount; x++)
{
System.out.println(x + ": " + message.getHex(x, x + 10));
bytes[x] = getByte(CHARACTER_7_BIT, x * 7 + offset);
}
return new String(bytes);
}
/**
* Parse ISO-8 bit characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseISO8(int offset, int characterCount)
{
byte[] bytes = new byte[characterCount];
for(int x = 0; x < characterCount; x++)
{
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes);
}
/**
* Parse UTF-8 characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
* @throws UnsupportedEncodingException if UTF-8 encoding is not supported.
*/
public String parseUTF8(int offset, int characterCount) throws UnsupportedEncodingException
{
byte[] bytes = new byte[characterCount];
for(int x = 0; x < characterCount; x++)
{
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes, UTF_8);
}
/**
* Parse 16-bit unicode characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseUnicode(int offset, int characterCount)
{
int length = characterCount * 2;
byte[] bytes = new byte[length];
for(int x = 0; x < length; x++)
{
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
}
return new String(bytes);
}
/**
* Parse 4-bit Binary Coded Decimal (BCD) characters from the message starting at the offset.
* @param offset where to start parsing.
* @param characterCount number of characters to parse
* @return parsed message.
*/
public String parseBCD4(int offset, int characterCount)
{
StringBuilder sb = new StringBuilder();
int count = 0;
while(count < characterCount)
{
int nibble = getNibble(offset);
//This will parse digits 0-9 and anything higher will parse as hexadecimal A-F
sb.append(Integer.toHexString(nibble).toUpperCase());
offset += 4;
count++;
}
return sb.toString();
}
}

View File

@@ -0,0 +1,70 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer;
/**
* Base native buffer class.
*/
public abstract class AbstractNativeBuffer implements INativeBuffer
{
private long mTimestamp;
private float mSamplesPerMillisecond;
/**
* Constructs an instance
* @param timestamp for the start of this buffer
* @param samplesPerMillisecond to calculate the time offset of sub-buffers extracted from this native buffer.
*/
public AbstractNativeBuffer(long timestamp, float samplesPerMillisecond)
{
mTimestamp = timestamp;
mSamplesPerMillisecond = samplesPerMillisecond;
}
/**
* Timestamp for the start of this buffer
* @return timestamp in milliseconds
*/
@Override
public long getTimestamp()
{
return mTimestamp;
}
/**
* Quantity of samples representing one millisecond of sample data, used for calculating fragment timestamp offsets.
* @return samples per millisecond count.
*/
public float getSamplesPerMillisecond()
{
return mSamplesPerMillisecond;
}
/**
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
* native buffer starting timestamp.
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
* @return timestamp adjusted to the fragment or sub-buffer start sample.
*/
protected long getFragmentTimestamp(int samplesPointer)
{
return getTimestamp() + (long)(samplesPointer / 2 / getSamplesPerMillisecond());
}
}

View File

@@ -0,0 +1,44 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer;
/**
* Base class for native buffer factories.
*/
public abstract class AbstractNativeBufferFactory implements INativeBufferFactory
{
private float mSamplesPerMillisecond = 0.0f;
@Override
public void setSamplesPerMillisecond(float samplesPerMillisecond)
{
mSamplesPerMillisecond = samplesPerMillisecond;
}
/**
* Quantity of I/Q sample pairs per milli-second at the current sample rate to use in calculating an accurate
* timestamp for sub-buffer that are generated from the native buffer.
* @return samples per millisecond.
*/
public float getSamplesPerMillisecond()
{
return mSamplesPerMillisecond;
}
}

View File

@@ -21,14 +21,13 @@ package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
* non-interleaved complex sample buffers converted from the raw byte sample array.
*/
public class ByteNativeBuffer implements INativeBuffer
public class ByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private final static float[] LOOKUP_VALUES;
@@ -42,21 +41,22 @@ public class ByteNativeBuffer implements INativeBuffer
for(int x = 0; x < 256; x++)
{
LOOKUP_VALUES[x] = (float)(x - 127) / 128.0f;
LOOKUP_VALUES[x] = ((float)x - 127.5f) / 128.0f;
}
}
private byte[] mSamples;
private long mTimestamp;
/**
* Constructs an instance
* @param samples to process
* @param timestamp of the samples
* @param averageDc measured from sample stream
* @param samplesPerMillisecond to calculate derivative timestamps for sub-buffers.
*/
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc)
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % FRAGMENT_SIZE != 0)
{
@@ -64,7 +64,6 @@ public class ByteNativeBuffer implements INativeBuffer
}
mSamples = samples;
mTimestamp = timestamp;
mAverageDc = averageDc;
}
@@ -74,12 +73,6 @@ public class ByteNativeBuffer implements INativeBuffer
return mSamples.length / 2;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
@Override
public Iterator<ComplexSamples> iterator()
{
@@ -105,6 +98,8 @@ public class ByteNativeBuffer implements INativeBuffer
@Override
public ComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] i = new float[FRAGMENT_SIZE];
float[] q = new float[FRAGMENT_SIZE];
int samplesOffset = mSamplesPointer;
@@ -116,8 +111,7 @@ public class ByteNativeBuffer implements INativeBuffer
}
mSamplesPointer = samplesOffset;
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
@@ -137,6 +131,8 @@ public class ByteNativeBuffer implements INativeBuffer
@Override
public InterleavedComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] converted = new float[FRAGMENT_SIZE * 2];
int samplesPointer = mSamplesPointer;
@@ -148,7 +144,7 @@ public class ByteNativeBuffer implements INativeBuffer
mSamplesPointer = samplesPointer;
return new InterleavedComplexSamples(converted, mTimestamp);
return new InterleavedComplexSamples(converted, timestamp);
}
}
}

View File

@@ -24,18 +24,22 @@ import java.nio.ByteBuffer;
/**
* Implements a factory for creating ByteNativeBuffer instances
*/
public class ByteNativeBufferFactory implements INativeBufferFactory
public class ByteNativeBufferFactory extends AbstractNativeBufferFactory
{
private static final float DC_FILTER_GAIN = 0.007f; //Normalizes DC over period of ~142 (1 / .007) buffers
private float mAverageDc = 0.0f;
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();
@Override
public INativeBuffer getBuffer(ByteBuffer samples, long timestamp)
{
byte[] copy = new byte[samples.capacity()];
samples.get(copy);
calculateDc(copy);
return new ByteNativeBuffer(copy, timestamp, mAverageDc);
if(mDcCorrectionManager.shouldCalculateDc())
{
calculateDc(copy);
}
return new ByteNativeBuffer(copy, timestamp, mDcCorrectionManager.getAverageDc(), getSamplesPerMillisecond());
}
/**
@@ -45,7 +49,7 @@ public class ByteNativeBufferFactory implements INativeBufferFactory
*/
private void calculateDc(byte[] samples)
{
double dcAccumulator = 0;
float dcAccumulator = 0;
for(byte sample: samples)
{
@@ -53,9 +57,8 @@ public class ByteNativeBufferFactory implements INativeBufferFactory
}
dcAccumulator /= samples.length;
dcAccumulator -= 127.0f;
dcAccumulator -= 127.5f;
dcAccumulator /= 128.0f;
dcAccumulator -= mAverageDc;
mAverageDc += (dcAccumulator * DC_FILTER_GAIN);
mDcCorrectionManager.adjust(dcAccumulator);
}
}

View File

@@ -1,20 +1,21 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2018 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
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.complex.Complex;

View File

@@ -0,0 +1,114 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer;
import java.util.concurrent.TimeUnit;
/**
* Provides DC offset correction value and manages calculation processing interval.
*/
public class DcCorrectionManager
{
// private static Logger mLog = LoggerFactory.getLogger(DcCorrectionManager.class);
// private DecimalFormat mDecimalFormat = new DecimalFormat("0.00000");
/**
* Time (delay) interval for running DC removal calculations
*/
private static final long DC_PROCESSING_INTERVAL = TimeUnit.SECONDS.toMillis(50);
/**
* Number of buffers to process per DC calculation processing interval.
*/
private static final int DC_CALCULATIONS_PER_INTERVAL = 5;
/**
* DC offset processing residual threshold for ending DC processing interval
*/
private static final float TARGET_DC_OFFSET_REMAINING = 0.0001f;
/**
* Number of DC calculations remaining in the current interval
*/
private int mDcCalculationsRemaining = DC_CALCULATIONS_PER_INTERVAL; //Initial value for coarse correction
/**
* Last time DC offset calculation interval ran
*/
private long mLastDcCalculationTimestamp = 0;
/**
* DC offset calculation gain.
*/
private static final float DC_FILTER_GAIN = 0.05f;
/**
* Current DC offset correction value
*/
private float mAverageDc = 0.0f;
public float getAverageDc()
{
return mAverageDc;
}
/**
* Adjusts the average DC based on the current calculated DC offset present in a sample buffer.
* @param calculatedDc for the current sample buffer
*/
public void adjust(float calculatedDc)
{
//Calculate the average scaled DC offset so that it can be applied in the native buffer's converted samples
float residualOffsetToRemove = calculatedDc - mAverageDc;
mAverageDc += (residualOffsetToRemove * DC_FILTER_GAIN);
if(Math.abs(residualOffsetToRemove) < TARGET_DC_OFFSET_REMAINING)
{
mDcCalculationsRemaining--;
}
//
// mLog.info("DC: " + mDecimalFormat.format(mAverageDc) +
// " RES:" + mDecimalFormat.format(residualOffsetToRemove) +
// " REM:" + mDcCalculationsRemaining);
}
/**
* Indicates if a DC offset calculation for a buffer should be performed.
*/
public boolean shouldCalculateDc()
{
if(System.currentTimeMillis() > (mLastDcCalculationTimestamp + DC_PROCESSING_INTERVAL))
{
if(mDcCalculationsRemaining > 0)
{
return true;
}
else
{
//Reset the number of buffers to calculate and the timestamp
mDcCalculationsRemaining = DC_CALCULATIONS_PER_INTERVAL;
mLastDcCalculationTimestamp = System.currentTimeMillis();
}
}
return false;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2021 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

View File

@@ -22,35 +22,24 @@ package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.SampleUtils;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Simple native buffer implementation that simply wraps a single, existing, non-native sample buffer.
*/
public class FloatNativeBuffer implements INativeBuffer
public class FloatNativeBuffer extends AbstractNativeBuffer
{
private float[] mInterleavedComplexSamples;
private long mTimestamp;
/**
* Constructs an instance
* @param complexSamples to wrap
* @param complexSamples interleaved to wrap
* @param timestamp for the buffer
*/
public FloatNativeBuffer(float[] complexSamples, long timestamp)
public FloatNativeBuffer(float[] complexSamples, long timestamp, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
mInterleavedComplexSamples = complexSamples;
mTimestamp = timestamp;
}
/**
* Constructs an instance
* @param complexSamples to wrap
*/
public FloatNativeBuffer(float[] complexSamples)
{
this(complexSamples, System.currentTimeMillis());
}
/**
@@ -58,9 +47,9 @@ public class FloatNativeBuffer implements INativeBuffer
* @param samples to wrap
* @param timestamp for the buffer
*/
public FloatNativeBuffer(ComplexSamples samples, long timestamp)
public FloatNativeBuffer(ComplexSamples samples)
{
this(SampleUtils.interleave(samples), timestamp);
this(SampleUtils.interleave(samples), samples.timestamp(), 0.0f);
}
/**
@@ -69,7 +58,7 @@ public class FloatNativeBuffer implements INativeBuffer
*/
public FloatNativeBuffer(InterleavedComplexSamples samples)
{
this(samples.samples(), samples.timestamp());
this(samples.samples(), samples.timestamp(), 0.0f);
}
@Override
@@ -90,12 +79,6 @@ public class FloatNativeBuffer implements INativeBuffer
return mInterleavedComplexSamples.length / 2;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
private class ComplexSamplesIterator implements Iterator<ComplexSamples>
{
private boolean mEmpty;
@@ -115,7 +98,7 @@ public class FloatNativeBuffer implements INativeBuffer
}
mEmpty = true;
return SampleUtils.deinterleave(mInterleavedComplexSamples);
return SampleUtils.deinterleave(mInterleavedComplexSamples, getTimestamp());
}
}
@@ -139,7 +122,7 @@ public class FloatNativeBuffer implements INativeBuffer
}
mEmpty = true;
return new InterleavedComplexSamples(mInterleavedComplexSamples, mTimestamp);
return new InterleavedComplexSamples(mInterleavedComplexSamples, getTimestamp());
}
}
}

View File

@@ -33,7 +33,15 @@ public interface INativeBufferFactory
*
* @param samples byte array copied from native memory
* @param timestamp of the samples
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
* @return instance
*/
INativeBuffer getBuffer(ByteBuffer samples, long timestamp);
/**
* Sets the samples per millisecond rate based on the current sample rate.
*
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
*/
void setSamplesPerMillisecond(float samplesPerMillisecond);
}

View File

@@ -1,6 +1,6 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 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
@@ -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.buffer;
/**

View File

@@ -21,14 +21,13 @@ package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.Iterator;
/**
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
* non-interleaved complex sample buffers converted from the raw byte sample array.
*/
public class SignedByteNativeBuffer implements INativeBuffer
public class SignedByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private final static float[] LOOKUP_VALUES;
@@ -47,7 +46,6 @@ public class SignedByteNativeBuffer implements INativeBuffer
}
private byte[] mSamples;
private long mTimestamp;
private float mIAverageDc;
private float mQAverageDc;
@@ -57,9 +55,12 @@ public class SignedByteNativeBuffer implements INativeBuffer
* @param timestamp of the samples
* @param iAverageDc of the sample stream
* @param qAverageDc of the sample stream
* @param samplesPerMillisecond to calculate sub-buffer timestamps
*/
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc)
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % FRAGMENT_SIZE != 0)
{
@@ -67,17 +68,10 @@ public class SignedByteNativeBuffer implements INativeBuffer
}
mSamples = samples;
mTimestamp = timestamp;
mIAverageDc = iAverageDc;
mQAverageDc = qAverageDc;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
@Override
public int sampleCount()
{
@@ -112,6 +106,8 @@ public class SignedByteNativeBuffer implements INativeBuffer
@Override
public ComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] i = new float[FRAGMENT_SIZE];
float[] q = new float[FRAGMENT_SIZE];
int samplesOffset = mSamplesPointer;
@@ -123,7 +119,7 @@ public class SignedByteNativeBuffer implements INativeBuffer
}
mSamplesPointer = samplesOffset;
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}
@@ -143,6 +139,8 @@ public class SignedByteNativeBuffer implements INativeBuffer
@Override
public InterleavedComplexSamples next()
{
long timestamp = getFragmentTimestamp(mSamplesPointer);
float[] converted = new float[FRAGMENT_SIZE * 2];
int samplesOffset = mSamplesPointer;
@@ -155,7 +153,7 @@ public class SignedByteNativeBuffer implements INativeBuffer
mSamplesPointer = samplesOffset;
return new InterleavedComplexSamples(converted, mTimestamp);
return new InterleavedComplexSamples(converted, timestamp);
}
}
}

View File

@@ -20,14 +20,51 @@
package io.github.dsheirer.buffer;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
/**
* Implements a factory for creating SignedByteNativeBuffer instances
*/
public class SignedByteNativeBufferFactory implements INativeBufferFactory
public class SignedByteNativeBufferFactory extends AbstractNativeBufferFactory
{
private static final float DC_FILTER_GAIN = 0.02f; //Normalizes DC over period of ~50 (1 / .02) buffers
/**
* DC removal calculations will run once a minute
*/
private static final long DC_PROCESSING_INTERVAL = TimeUnit.SECONDS.toMillis(60);
/**
* Number of buffers to process per DC calculation processing interval.
*/
private static final int DC_CALCULATIONS_PER_INTERVAL = 5;
/**
* DC offset processing residual threshold for ending DC processing interval
*/
private static final float TARGET_DC_OFFSET_REMAINING = 0.0002f;
/**
* Number of DC calculations remaining in the current interval
*/
private int mDcCalculationsRemaining = DC_CALCULATIONS_PER_INTERVAL; //Initial value for coarse correction
/**
* Last time DC offset calculation interval ran
*/
private long mLastDcCalculationTimestamp = 0;
/**
* DC offset calculation gain.
*/
private static final float DC_FILTER_GAIN = 0.05f;
/**
* Current DC offset correction for Inphase samples
*/
private float mIAverageDc = 0.0f;
/**
* Current DC offset correction for Quadrature samples
*/
private float mQAverageDc = 0.0f;
@Override
@@ -35,8 +72,35 @@ public class SignedByteNativeBufferFactory implements INativeBufferFactory
{
byte[] copy = new byte[samples.capacity()];
samples.get(copy);
calculateDc(copy);
return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc);
if(shouldCalculateDc())
{
calculateDc(copy);
}
return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc, getSamplesPerMillisecond());
}
/**
* Indicates if a DC offset calculation for a buffer should be performed.
*/
private boolean shouldCalculateDc()
{
if(System.currentTimeMillis() > (mLastDcCalculationTimestamp + DC_PROCESSING_INTERVAL))
{
if(mDcCalculationsRemaining > 0)
{
return true;
}
else
{
//Reset the number of buffers to calculate and the timestamp
mDcCalculationsRemaining = DC_CALCULATIONS_PER_INTERVAL;
mLastDcCalculationTimestamp = System.currentTimeMillis();
}
}
return false;
}
/**
@@ -46,8 +110,8 @@ public class SignedByteNativeBufferFactory implements INativeBufferFactory
*/
private void calculateDc(byte[] samples)
{
double iDcAccumulator = 0;
double qDcAccumulator = 0;
float iDcAccumulator = 0;
float qDcAccumulator = 0;
for(int x = 0; x < samples.length; x += 2)
{
@@ -64,5 +128,10 @@ public class SignedByteNativeBufferFactory implements INativeBufferFactory
qDcAccumulator /= 128.0f;
qDcAccumulator -= mQAverageDc;
mQAverageDc += (qDcAccumulator * DC_FILTER_GAIN);
if(Math.abs(iDcAccumulator) < TARGET_DC_OFFSET_REMAINING && Math.abs(qDcAccumulator) < TARGET_DC_OFFSET_REMAINING)
{
mDcCalculationsRemaining--;
}
}
}

View File

@@ -20,7 +20,6 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
import java.util.Iterator;
/**
@@ -42,8 +41,9 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
protected float[] mQBuffer = new float[FRAGMENT_SIZE + Q_OVERLAP];
protected short[] mSamples;
protected int mSamplesPointer = 0;
protected long mTimestamp;
protected float mAverageDc;
private long mTimestamp;
private float mSamplesPerMillisecond;
/**
* Constructs an instance
@@ -52,8 +52,10 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
* @param residualQ samples from previous buffer
* @param averageDc as measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
float samplesPerMillisecond)
{
if(residualI.length != I_OVERLAP || residualQ.length != Q_OVERLAP)
{
@@ -73,6 +75,7 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
mAverageDc = averageDc;
mSamples = samples;
mTimestamp = timestamp;
mSamplesPerMillisecond = samplesPerMillisecond;
//Transfer and scale the residual I & Q samples from the previous buffer
for(int i = 0; i < residualI.length; i++)
@@ -86,6 +89,17 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
}
}
/**
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
* native buffer starting timestamp.
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
* @return timestamp adjusted to the fragment or sub-buffer start sample.
*/
protected long getFragmentTimestamp(int samplesPointer)
{
return mTimestamp + (long)(samplesPointer / 2 / mSamplesPerMillisecond);
}
@Override
public boolean hasNext()
{

View File

@@ -20,7 +20,6 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
/**
@@ -36,10 +35,11 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -50,6 +50,8 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
for(int x = 0; x < FRAGMENT_SIZE; x++)
@@ -91,6 +93,6 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -55,6 +56,7 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -121,6 +123,6 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -55,6 +56,7 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -115,6 +117,6 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -42,10 +41,12 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
mFilterPart2 = new float[16];
System.arraycopy(COEFFICIENTS, 16, mFilterPart2, 8, 8);
@@ -59,6 +60,8 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -117,6 +120,6 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -20,12 +20,11 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.util.Arrays;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
*/
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -55,6 +56,8 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -133,6 +136,6 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, timestamp);
}
}

View File

@@ -34,11 +34,12 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -49,6 +50,7 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
for(int x = 0; x < FRAGMENT_SIZE; x++)
@@ -89,6 +91,6 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -118,6 +120,6 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -112,6 +114,6 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -40,11 +40,12 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
mFilterPart2 = new float[16];
@@ -59,6 +60,7 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -115,6 +117,6 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp)
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp);
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}
@Override
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
throw new IllegalStateException("End of buffer exceeded");
}
long timestamp = getFragmentTimestamp(mSamplesPointer);
int offset = mSamplesPointer;
int fragmentPointer = 0;
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
@@ -130,6 +132,6 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
return new InterleavedComplexSamples(samples, mTimestamp);
return new InterleavedComplexSamples(samples, timestamp);
}
}

View File

@@ -19,23 +19,21 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.vector.calibrate.Implementation;
import java.util.Iterator;
/**
* Native buffer scalar implementation for Airspy non-packed samples.
*/
public class AirspyNativeBuffer implements INativeBuffer
public class AirspyNativeBuffer extends AbstractNativeBuffer
{
private short[] mSamples;
private short[] mResidualI;
private short[] mResidualQ;
private float mAverageDc;
private long mTimestamp;
private Implementation mInterleavedImplementation;
private Implementation mNonInterleavedImplementation;
@@ -48,11 +46,14 @@ public class AirspyNativeBuffer implements INativeBuffer
* @param timestamp of the buffer
* @param interleavedImplementation optimal, scalar vs vector SIMD
* @param nonInterleavedImplementation optimal, scalar vs vector SIMD
* @param samplesPerMillisecond used to calculate sub-buffer fragment timestamp offsets from the start of this buffer.
*/
public AirspyNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, Implementation interleavedImplementation,
Implementation nonInterleavedImplementation)
Implementation nonInterleavedImplementation, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % AirspyBufferIterator.FRAGMENT_SIZE != 0)
{
@@ -64,7 +65,6 @@ public class AirspyNativeBuffer implements INativeBuffer
mResidualI = residualI;
mResidualQ = residualQ;
mAverageDc = averageDc;
mTimestamp = timestamp;
mInterleavedImplementation = interleavedImplementation;
mNonInterleavedImplementation = nonInterleavedImplementation;
}
@@ -75,22 +75,16 @@ public class AirspyNativeBuffer implements INativeBuffer
return mSamples.length / 2;
}
@Override
public long getTimestamp()
{
return mTimestamp;
}
@Override
public Iterator<ComplexSamples> iterator()
{
return switch(mInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}
@@ -99,11 +93,11 @@ public class AirspyNativeBuffer implements INativeBuffer
{
return switch(mInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}
}

View File

@@ -19,19 +19,18 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.INativeBufferFactory;
import io.github.dsheirer.vector.calibrate.CalibrationManager;
import io.github.dsheirer.vector.calibrate.CalibrationType;
import io.github.dsheirer.vector.calibrate.Implementation;
import java.nio.ByteBuffer;
import java.util.Arrays;
/**
* Implements a factory for creating SignedByteNativeBuffer instances
*/
public class AirspyNativeBufferFactory implements INativeBufferFactory
public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
{
private boolean mSamplePacking = false;
private short[] mResidualI = new short[AirspyBufferIterator.I_OVERLAP];
@@ -106,7 +105,7 @@ public class AirspyNativeBufferFactory implements INativeBufferFactory
INativeBuffer nativeBuffer = new AirspyNativeBuffer(samples,
Arrays.copyOf(mResidualI, mResidualI.length),
Arrays.copyOf(mResidualQ, mResidualQ.length), mConverter.getAverageDc(), timestamp,
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation);
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation, getSamplesPerMillisecond());
extractResidual(samples);

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.DcCorrectionManager;
import java.nio.ByteBuffer;
/**
@@ -26,40 +27,61 @@ import java.nio.ByteBuffer;
*/
public class ScalarPackedSampleConverter implements IAirspySampleConverter
{
private static final float DC_FILTER_GAIN = 0.007f; //Normalizes DC over period of ~142 (1 / .007) buffers
private float mAverageDc = 0.0f;
/**
* Manages DC calculations and processing interval
*/
private DcCorrectionManager mDcCalculationManager = new DcCorrectionManager();
@Override
public short[] convert(ByteBuffer buffer)
{
boolean shouldCalculateDc = mDcCalculationManager.shouldCalculateDc();
int offset = 0;
long dcAccumulator = 0;
short sample;
short[] samples;
byte b1, b2, b3;
samples = new short[buffer.capacity() / 3 * 2];
for(int x = 0; x < samples.length; x += 2)
if(shouldCalculateDc)
{
b1 = buffer.get(offset++);
b2 = buffer.get(offset++);
b3 = buffer.get(offset++);
long dcAccumulator = 0;
sample = (short)(((b1 & 0xFF) << 4) | ((b2 & 0xF0) >> 4));
samples[x] = sample;
dcAccumulator += sample;
for(int x = 0; x < samples.length; x += 2)
{
b1 = buffer.get(offset++);
b2 = buffer.get(offset++);
b3 = buffer.get(offset++);
sample = (short)(((b2 & 0xF) << 8) | (b3 & 0xFF));
samples[x] = sample;
dcAccumulator += sample;
sample = (short)(((b1 & 0xFF) << 4) | ((b2 & 0xF0) >> 4));
samples[x] = sample;
dcAccumulator += sample;
sample = (short)(((b2 & 0xF) << 8) | (b3 & 0xFF));
samples[x] = sample;
dcAccumulator += sample;
}
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
mDcCalculationManager.adjust(averageDcNow);
}
else
{
for(int x = 0; x < samples.length; x += 2)
{
b1 = buffer.get(offset++);
b2 = buffer.get(offset++);
b3 = buffer.get(offset++);
//Calculate the average scaled DC offset so that it can be applied in the native buffer's converted samples
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
averageDcNow -= mAverageDc;
mAverageDc += (averageDcNow * DC_FILTER_GAIN);
sample = (short)(((b1 & 0xFF) << 4) | ((b2 & 0xF0) >> 4));
samples[x] = sample;
sample = (short)(((b2 & 0xF) << 8) | (b3 & 0xFF));
samples[x] = sample;
}
}
return samples;
}
@@ -67,7 +89,7 @@ public class ScalarPackedSampleConverter implements IAirspySampleConverter
@Override
public float getAverageDc()
{
return mAverageDc;
return mDcCalculationManager.getAverageDc();
}
public static void main(String[] args)

View File

@@ -19,6 +19,7 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.DcCorrectionManager;
import java.nio.ByteBuffer;
/**
@@ -26,34 +27,50 @@ import java.nio.ByteBuffer;
*/
public class ScalarUnpackedSampleConverter implements IAirspySampleConverter
{
private static final float DC_FILTER_GAIN = 0.007f; //Normalizes DC over period of ~142 (1 / .007) buffers
private float mAverageDc = 0.0f;
/**
* Manages DC calculations and processing interval
*/
private DcCorrectionManager mDcCalculationManager = new DcCorrectionManager();
@Override
public short[] convert(ByteBuffer buffer)
{
boolean shouldCalculateDc = mDcCalculationManager.shouldCalculateDc();
int offset = 0;
long dcAccumulator = 0;
short sample;
short[] samples;
byte b1, b2;
samples = new short[buffer.capacity() / 2];
for(int x = 0; x < samples.length; x++)
if(shouldCalculateDc)
{
b1 = buffer.get(offset++);
b2 = buffer.get(offset++);
sample = (short)(((b2 & 0x0F) << 8) | (b1 & 0xFF));
samples[x] = sample;
dcAccumulator += sample;
}
long dcAccumulator = 0;
//Calculate the average scaled DC offset so that it can be applied in the native buffer's converted samples
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
averageDcNow -= mAverageDc;
mAverageDc += (averageDcNow * DC_FILTER_GAIN);
for(int x = 0; x < samples.length; x++)
{
b1 = buffer.get(offset++);
b2 = buffer.get(offset++);
sample = (short)(((b2 & 0x0F) << 8) | (b1 & 0xFF));
samples[x] = sample;
dcAccumulator += sample;
}
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
mDcCalculationManager.adjust(averageDcNow);
}
else
{
for(int x = 0; x < samples.length; x++)
{
b1 = buffer.get(offset++);
b2 = buffer.get(offset++);
sample = (short)(((b2 & 0x0F) << 8) | (b1 & 0xFF));
samples[x] = sample;
}
}
return samples;
}
@@ -61,6 +78,6 @@ public class ScalarUnpackedSampleConverter implements IAirspySampleConverter
@Override
public float getAverageDc()
{
return mAverageDc;
return mDcCalculationManager.getAverageDc();
}
}

View File

@@ -19,25 +19,29 @@
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.DcCorrectionManager;
import java.nio.ByteBuffer;
import jdk.incubator.vector.ShortVector;
import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
import java.nio.ByteBuffer;
/**
* Scalar implementation of airspy sample converter for un-packed samples.
*/
public class VectorUnpackedSampleConverter implements IAirspySampleConverter
{
private static final VectorSpecies<Short> VECTOR_SPECIES = ShortVector.SPECIES_PREFERRED;
private static final float DC_FILTER_GAIN = 0.007f; //Normalizes DC over period of ~142 (1 / .007) buffers
private float mAverageDc = 0.0f;
/**
* Manages DC calculations and processing interval
*/
private DcCorrectionManager mDcCalculationManager = new DcCorrectionManager();
@Override
public short[] convert(ByteBuffer buffer)
{
long dcAccumulator = 0;
boolean shouldCalculateDc = mDcCalculationManager.shouldCalculateDc();
short sample;
short[] samples;
byte b1, b2;
@@ -53,44 +57,73 @@ public class VectorUnpackedSampleConverter implements IAirspySampleConverter
short[] bytes1 = new short[VECTOR_SPECIES.length()];
short[] bytes2 = new short[VECTOR_SPECIES.length()];
for(; samplesOffset < VECTOR_SPECIES.loopBound(samples.length); samplesOffset += VECTOR_SPECIES.length())
if(shouldCalculateDc)
{
bytesOffset = 0;
long dcAccumulator = 0;
while(bytesOffset < VECTOR_SPECIES.length())
for(; samplesOffset < VECTOR_SPECIES.loopBound(samples.length); samplesOffset += VECTOR_SPECIES.length())
{
bytes1[bytesOffset] = (short)(buffer.get(rawPointer++) & 0xFF);
bytes2[bytesOffset++] = (short)(buffer.get(rawPointer++) & 0x0F);
bytesOffset = 0;
while(bytesOffset < VECTOR_SPECIES.length())
{
bytes1[bytesOffset] = (short)(buffer.get(rawPointer++) & 0xFF);
bytes2[bytesOffset++] = (short)(buffer.get(rawPointer++) & 0x0F);
}
vector = ShortVector.fromArray(VECTOR_SPECIES, bytes1, 0)
.or(ShortVector.fromArray(VECTOR_SPECIES, bytes2, 0).lanewise(VectorOperators.LSHL, 8));
dcAccumulator += vector.reduceLanes(VectorOperators.ADD);
vector.intoArray(samples, samplesOffset);
}
vector = ShortVector.fromArray(VECTOR_SPECIES, bytes1, 0)
.or(ShortVector.fromArray(VECTOR_SPECIES, bytes2, 0).lanewise(VectorOperators.LSHL, 8));
dcAccumulator += vector.reduceLanes(VectorOperators.ADD);
vector.intoArray(samples, samplesOffset);
}
for(; samplesOffset < samples.length; samplesOffset++)
{
b1 = buffer.get(rawPointer++);
b2 = buffer.get(rawPointer++);
for(; samplesOffset < samples.length; samplesOffset++)
sample = (short)(((b2 & 0x0F) << 8) | (b1 & 0xFF));
samples[samplesOffset] = sample;
dcAccumulator += sample;
}
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
mDcCalculationManager.adjust(averageDcNow);
}
else
{
b1 = buffer.get(rawPointer++);
b2 = buffer.get(rawPointer++);
for(; samplesOffset < VECTOR_SPECIES.loopBound(samples.length); samplesOffset += VECTOR_SPECIES.length())
{
bytesOffset = 0;
sample = (short)(((b2 & 0x0F) << 8) | (b1 & 0xFF));
samples[samplesOffset] = sample;
dcAccumulator += sample;
while(bytesOffset < VECTOR_SPECIES.length())
{
bytes1[bytesOffset] = (short)(buffer.get(rawPointer++) & 0xFF);
bytes2[bytesOffset++] = (short)(buffer.get(rawPointer++) & 0x0F);
}
vector = ShortVector.fromArray(VECTOR_SPECIES, bytes1, 0)
.or(ShortVector.fromArray(VECTOR_SPECIES, bytes2, 0).lanewise(VectorOperators.LSHL, 8));
vector.intoArray(samples, samplesOffset);
}
for(; samplesOffset < samples.length; samplesOffset++)
{
b1 = buffer.get(rawPointer++);
b2 = buffer.get(rawPointer++);
sample = (short)(((b2 & 0x0F) << 8) | (b1 & 0xFF));
samples[samplesOffset] = sample;
}
}
//Calculate the average scaled DC offset so that it can be applied in the native buffer's converted samples
float averageDcNow = ((float)dcAccumulator / (float)samples.length) - 2048.0f;
averageDcNow *= AirspyBufferIterator.SCALE_SIGNED_12_BIT_TO_FLOAT;
averageDcNow -= mAverageDc;
mAverageDc += (averageDcNow * DC_FILTER_GAIN);
return samples;
}
@Override
public float getAverageDc()
{
return mAverageDc;
return mDcCalculationManager.getAverageDc();
}
}

View File

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

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.channel.metadata;
@@ -28,10 +27,11 @@ import io.github.dsheirer.module.decode.event.DecodeEventPanel;
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.settings.SettingsManager;
import java.awt.Color;
import net.miginfocom.swing.MigLayout;
import javax.swing.JPanel;
import java.awt.Color;
public class NowPlayingPanel extends JPanel
{
@@ -45,13 +45,14 @@ public class NowPlayingPanel extends JPanel
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
* messages, events, and spectral view.
*/
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences)
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
SettingsManager settingsManager)
{
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);
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
init();
}
@@ -68,6 +69,10 @@ public class NowPlayingPanel extends JPanel
tabbedPane.setFont(this.getFont());
tabbedPane.setForeground(Color.BLACK);
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
tabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(tabbedPane.getSelectedIndex() == tabbedPane
.indexOfComponent(mChannelPowerPanel)));
JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
splitPane.setShowGripper(true);
splitPane.add(mChannelMetadataPanel);

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -30,8 +30,10 @@ 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.SourceManager;
import io.github.dsheirer.source.tuner.TunerViewPanel;
import io.github.dsheirer.source.tuner.manager.TunerManager;
import io.github.dsheirer.source.tuner.ui.TunerViewPanel;
import java.awt.Color;
import java.awt.Dimension;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
@@ -41,8 +43,6 @@ import org.slf4j.LoggerFactory;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.Dimension;
public class ControllerPanel extends JPanel
{
@@ -59,13 +59,13 @@ public class ControllerPanel extends JPanel
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
SourceManager sourceManager, UserPreferences userPreferences)
TunerManager tunerManager, UserPreferences userPreferences)
{
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, sourceManager, audioPlaybackManager,
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
playlistManager.getAliasModel());
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences);
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
mTunerManagerPanel = new TunerViewPanel(sourceManager.getTunerModel(), userPreferences);
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
init();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 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

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 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;
@@ -28,6 +25,7 @@ import io.github.dsheirer.channel.metadata.ChannelAndMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadata;
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.identifier.Form;
@@ -47,14 +45,12 @@ import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.config.SourceConfigTuner;
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 javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.GraphicsEnvironment;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -64,6 +60,9 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javafx.application.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Channel processing manager handles all starting and stopping of channel decoding. A processing chain is created
@@ -86,7 +85,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
private ChannelMapModel mChannelMapModel;
private ChannelMetadataModel mChannelMetadataModel;
private EventLogManager mEventLogManager;
private SourceManager mSourceManager;
private TunerManager mTunerManager;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private List<Long> mLoggedFrequencies = new ArrayList<>();
@@ -97,16 +96,16 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*
* @param channelMapModel containing channel maps defined by the user
* @param eventLogManager for adding event loggers to channels
* @param sourceManager for obtaining a tuner channel source for the channel
* @param tunerManager for obtaining a tuner channel source for the channel
* @param aliasModel for aliasing of identifiers produced by the channel
* @param userPreferences for user defined behavior and settings
*/
public ChannelProcessingManager(ChannelMapModel channelMapModel, EventLogManager eventLogManager,
SourceManager sourceManager, AliasModel aliasModel, UserPreferences userPreferences)
TunerManager tunerManager, AliasModel aliasModel, UserPreferences userPreferences)
{
mChannelMapModel = channelMapModel;
mEventLogManager = eventLogManager;
mSourceManager = sourceManager;
mTunerManager = tunerManager;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mChannelMetadataModel = new ChannelMetadataModel();
@@ -126,7 +125,20 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
private boolean isProcessing(Channel channel)
{
return mProcessingChains.containsKey(channel) && mProcessingChains.get(channel).isProcessing();
boolean isProcessing = false;
mLock.lock();
try
{
isProcessing = mProcessingChains.containsKey(channel) && mProcessingChains.get(channel).isProcessing();
}
finally
{
mLock.unlock();
}
return isProcessing;
}
/**
@@ -155,18 +167,60 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
public Channel getChannel(ProcessingChain processingChain)
{
Channel channel = null;
if(processingChain != null)
{
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
mLock.lock();
try
{
if(entry.getValue() == processingChain)
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
{
return entry.getKey();
if(entry.getValue() == processingChain)
{
channel = entry.getKey();
break;
}
}
}
finally
{
mLock.unlock();
}
}
return null;
return channel;
}
/**
* Retrieves the channel associated with the processing chain that is consuming from the tuner channel source
* @param tunerChannelSource to find the channel
* @return channel
*/
private Channel getChannel(TunerChannelSource tunerChannelSource)
{
Channel channel = null;
mLock.lock();
try
{
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
{
if(entry.getValue().hasSource(tunerChannelSource))
{
channel = entry.getKey();
break;
}
}
}
finally
{
mLock.unlock();
}
return channel;
}
/**
@@ -275,6 +329,28 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
}
/**
* Request to stop a channel that is currently processing
* @param request with the tuner channel source feeding the channel to be stopped.
*/
@Subscribe
public void stopChannelRequest(ChannelStopProcessingRequest request)
{
Channel channel = getChannel(request.getTunerChannelSource());
if(channel != null)
{
try
{
stop(channel);
}
catch(ChannelException ce)
{
mLog.error("Error stopping channel [" + channel + "]", ce);
}
}
}
/**
* Stops the specified channel
* @param channel to stop
@@ -303,7 +379,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
try
{
source = mSourceManager.getSource(channel.getSourceConfiguration(),
source = mTunerManager.getSource(channel.getSourceConfiguration(),
channel.getDecodeConfiguration().getChannelSpecification());
}
catch(SourceException se)
@@ -439,8 +515,15 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
{
processingChain.start();
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(true));
if(GraphicsEnvironment.isHeadless())
{
channel.setProcessing(true);
}
else
{
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(true));
}
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_START));
}
@@ -526,13 +609,20 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
*/
private void stopProcessing(Channel channel) throws ChannelException
{
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(false));
ProcessingChain processingChain = removeProcessingChain(channel);
if(processingChain != null)
{
if(GraphicsEnvironment.isHeadless())
{
channel.setProcessing(false);
}
else
{
//This has to be done on the FX event thread when the playlist editor is constructed
Platform.runLater(() -> channel.setProcessing(false));
}
//Since the processing chain's source will block on stop(), throw it to the thread pool to run
ThreadPool.CACHED.submit(() -> {
try
@@ -558,10 +648,6 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
}
});
}
else
{
throw new ChannelException("Channel is not currently playing");
}
}
/**
@@ -736,7 +822,14 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
if(toShutdown != null)
{
mLog.warn("Channel source error detected - stopping channel [" + toShutdown.getName() + "]");
if(sourceEvent.getEvent() == SourceEvent.Event.NOTIFICATION_ERROR_STATE)
{
mLog.warn("Channel source error detected - stopping channel [" + toShutdown.getName() + "]");
}
else
{
mLog.warn("Source event error - stopping channel [" + toShutdown.getName() + "]");
}
try
{

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2018 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.controller.channel;
@@ -33,7 +32,7 @@ public class ChannelSelectionManager implements Listener<ChannelEvent>
public ChannelSelectionManager(ChannelModel channelModel)
{
mChannelEventListener = channelModel;
channelModel.addListener(this::receive);
channelModel.addListener(ChannelSelectionManager.this);
}
@Override

View File

@@ -0,0 +1,44 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.controller.channel.event;
import io.github.dsheirer.module.ModuleEventBusMessage;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
/**
* Request to stop processing a channel for a specific tuner channel source
*/
public class ChannelStopProcessingRequest extends ModuleEventBusMessage
{
private TunerChannelSource mTunerChannelSource;
/**
* Constructs an instance
* @param tunerChannelSource for the channel to stop
*/
public ChannelStopProcessingRequest(TunerChannelSource tunerChannelSource)
{
mTunerChannelSource = tunerChannelSource;
}
public TunerChannelSource getTunerChannelSource()
{
return mTunerChannelSource;
}
}

View File

@@ -23,11 +23,10 @@ import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.SourceEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractComplexPolyphaseChannelizer implements Listener<InterleavedComplexSamples>, ISourceEventListener
{
@@ -38,6 +37,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
private int mChannelCount;
private int mSubChannelCount;
private double mChannelSampleRate;
protected long mCurrentSamplesTimestamp;
/**
* Complex sample polyphase channelizer
@@ -106,7 +106,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
{
for(PolyphaseChannelSource channel : mChannels)
{
channel.receiveChannelResults(channelResultsList);
channel.receiveChannelResults(channelResultsList, mCurrentSamplesTimestamp);
}
}

View File

@@ -1,31 +1,34 @@
/*******************************************************************************
* *********************************************************************************************************************
* sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
* *********************************************************************************************************************
******************************************************************************/
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class ChannelCalculator
{
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class);
/**
@@ -546,13 +549,13 @@ public class ChannelCalculator
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Channel Calculator Settings");
sb.append(" Channels:").append(getChannelCount());
sb.append(" Sample Rate:").append(getSampleRate());
sb.append(" Channel Bandwidth:").append(getChannelBandwidth());
sb.append(" Channel Rate:").append(getChannelSampleRate());
sb.append(" Min:").append(getMinimumFrequency());
sb.append(" Max:").append(getMaximumFrequency());
sb.append("Channel Calculator | Tuner SR:").append(FREQUENCY_FORMAT.format(getSampleRate() / 1E6D));
sb.append(" CF:").append(FREQUENCY_FORMAT.format(getCenterFrequency() / 1E6D));
sb.append(" MIN:").append(FREQUENCY_FORMAT.format(getMinimumFrequency() / 1E6D));
sb.append(" MAX:").append(FREQUENCY_FORMAT.format(getMaximumFrequency() / 1E6D));
sb.append(" | Channel COUNT:").append(getChannelCount());
sb.append(" BW:").append(FREQUENCY_FORMAT.format(getChannelBandwidth() / 1E6D));
sb.append(" SR:").append(FREQUENCY_FORMAT.format(getChannelSampleRate() / 1E6D));
return sb.toString();
}
}

View File

@@ -22,15 +22,14 @@ import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import io.github.dsheirer.util.Dispatcher;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample
@@ -193,7 +192,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
@Override
public void receive(InterleavedComplexSamples complexSamples)
{
//TODO: how do we pass the timestamp onto the sample assemblers?
mCurrentSamplesTimestamp = complexSamples.timestamp();
float[] samples = complexSamples.samples();
int samplesPointer = 0;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,11 +21,9 @@ package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.buffer.INativeBuffer;
import io.github.dsheirer.buffer.INativeBufferProvider;
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
@@ -37,19 +35,19 @@ import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import io.github.dsheirer.util.Dispatcher;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.commons.math3.util.FastMath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
@@ -69,21 +67,23 @@ import java.util.concurrent.CopyOnWriteArrayList;
*/
public class PolyphaseChannelManager implements ISourceEventProcessor
{
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
private static final double CHANNEL_OVERSAMPLING = 2.0;
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
private INativeBufferProvider mNativeBufferProvider;
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
private ChannelCalculator mChannelCalculator;
private SynthesisFilterManager mFilterManager = new SynthesisFilterManager();
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
private NativeBufferReceiver mNativeBufferReceiver = new NativeBufferReceiver();
private Dispatcher mBufferDispatcher;
private Map<Integer,float[]> mOutputProcessorFilters = new HashMap<>();
private boolean mRunning = true;
/**
* Creates a polyphase channel manager instance.
@@ -126,6 +126,46 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
}
/**
* Provides a description of the state of this manager.
*/
public String getStateDescription()
{
StringBuilder sb = new StringBuilder();
sb.append("Polyphase Channel Manager Providing [").append(mChannelSources.size()).append("] Channels");
sb.append("\n\t").append(mChannelCalculator);
for(PolyphaseChannelSource pcs: mChannelSources)
{
List<Integer> indexes = pcs.getOutputProcessorIndexes();
double sampleRate = pcs.getSampleRate();
long indexCenterFrequency = pcs.getIndexCenterFrequency();
long appliedFrequencyOffset = pcs.getFrequencyOffset();
long requestedCenterFrequency = pcs.getFrequency();
sb.append("\n\tPolyphase | Tuner SR:").append(FREQUENCY_FORMAT.format(pcs.getTunerSampleRate() / 1E6d));
sb.append(" CF:").append(FREQUENCY_FORMAT.format(pcs.getTunerCenterFrequency() / 1E6d));
sb.append(" BW: ").append(FREQUENCY_FORMAT.format(sampleRate / 1E6d));
sb.append(" | Channel CF: ").append(FREQUENCY_FORMAT.format(indexCenterFrequency / 1E6d));
sb.append(" REQUESTED CF: ").append(FREQUENCY_FORMAT.format(requestedCenterFrequency / 1E6d));
sb.append(" MIXER:").append(FREQUENCY_FORMAT.format(appliedFrequencyOffset / 1E6d));
sb.append(" | Polyphase Indices: ").append(indexes);
}
return sb.toString();
}
public void stopAllChannels()
{
mRunning = false;
List<TunerChannelSource> toStop = new ArrayList<>(mChannelSources);
for(TunerChannelSource tunerChannelSource: toStop)
{
MyEventBus.getGlobalEventBus().post(new ChannelStopProcessingRequest(tunerChannelSource));
}
}
/**
* Signals to all provisioned tuner channel sources that the source complex buffer provider has an error and can
* no longer provide channels, so that the tuner channel source can notify the consumer of the error state.
@@ -164,22 +204,16 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
{
PolyphaseChannelSource channelSource = null;
List<Integer> polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
if(outputProcessor != null)
if(mRunning)
{
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
try
{
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
mChannelCalculator.getChannelSampleRate(), centerFrequency);
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
mChannelSourceEventListener);
mChannelSources.add(channelSource);
}
catch(FilterDesignException fde)
catch(IllegalArgumentException iae)
{
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
}
@@ -188,38 +222,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
return channelSource;
}
/**
* Creates a processor to process the channelizer channel indexes into a composite output stream providing
* channelized complex sample buffers to a registered source listener.
* @param indexes to target by the output processor
* @return output processor compatible with the number of indexes to monitor
*/
private IPolyphaseChannelOutputProcessor getOutputProcessor(List<Integer> indexes)
{
switch(indexes.size())
{
case 1:
return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes,
mChannelCalculator.getChannelCount());
case 2:
try
{
float[] filter = getOutputProcessorFilter(2);
return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes, filter,
mChannelCalculator.getChannelCount());
}
catch(FilterDesignException fde)
{
mLog.error("Error designing 2 channel synthesis filter for output processor");
}
default:
//TODO: create output processor for greater than 2 input channels
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(mChannelCalculator.toString());
return null;
}
}
/**
* Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive
* sample buffers when this is the first channel.
@@ -231,7 +233,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
synchronized(mBufferDispatcher)
{
//Note: the polyphase channel source has already been added to the mChannelSources in getChannel() method
checkChannelizerConfiguration();
mPolyphaseChannelizer.addChannel(channelSource);
@@ -293,10 +294,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
switch(sourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
//Update channel calculator immediately so that channels can be allocated
mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());
//Defer channelizer configuration changes to be handled on the buffer processor thread
mNativeBufferReceiver.receive(sourceEvent);
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
@@ -312,7 +309,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
//no-op
break;
default:
mLog.info("Unrecognized source event: " + sourceEvent.toString());
mLog.info("Unrecognized source event: " + sourceEvent);
break;
}
}
@@ -363,79 +360,21 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
* Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which
* would cause the output processors to change the polyphase channelizer results channel(s) that the processor is
* consuming
*
* @param sourceEvent (optional-can be null) to broadcast to each output processor following the update
*/
private void updateOutputProcessors(SourceEvent sourceEvent)
private void updateOutputProcessors()
{
for(PolyphaseChannelSource channelSource: mChannelSources)
{
updateOutputProcessor(channelSource);
//Send the non-null source event to each channel source
if(sourceEvent != null)
try
{
try
{
channelSource.process(sourceEvent);
}
catch(SourceException se)
{
mLog.error("Error while notifying polyphase channel source of a source event", se);
}
channelSource.updateOutputProcessor(mChannelCalculator, mFilterManager);
}
}
}
/**
* Updates the polyphase channel source's output processor due to a change in the center frequency or sample
* rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's
* center tuned frequency changes.
*
* @param channelSource that requires an update to its output processor
*/
private void updateOutputProcessor(PolyphaseChannelSource channelSource)
{
try
{
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel());
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes);
//If the indexes size is the same then update the current processor, otherwise create a new one
IPolyphaseChannelOutputProcessor outputProcessor = channelSource.getPolyphaseChannelOutputProcessor();
if(outputProcessor != null && outputProcessor.getInputChannelCount() == indexes.size())
catch(IllegalArgumentException iae)
{
channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
channelSource.setFrequency(centerFrequency);
if(indexes.size() > 1)
{
try
{
float[] filter = getOutputProcessorFilter(indexes.size());
channelSource.getPolyphaseChannelOutputProcessor().setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
}
}
mLog.error("Error updating polyphase channel source output processor following tuner frequency or " +
"sample rate change");
stopChannelSource(channelSource);
}
else
{
channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency);
}
}
catch(IllegalArgumentException iae)
{
mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " +
"updated tuner center frequency and sample rate. Stopping channel source", iae);
stopChannelSource(channelSource);
}
}
@@ -479,29 +418,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
mSourceEventBroadcaster.removeListener(listener);
}
/**
* Generates (or reuses) an output processor filter for the specified number of channels. Each
* filter is created only once and stored in a map for reuse. This map is cleared anytime that the
* input sample rate changes, so that the filters can be recreated with the new channel sample rate.
* @param channels count
* @return filter
* @throws FilterDesignException if the filter cannot be designed to specification (-6 dB band edge)
*/
private float[] getOutputProcessorFilter(int channels) throws FilterDesignException
{
float[] taps = mOutputProcessorFilters.get(channels);
if(taps == null)
{
taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelSampleRate(),
mChannelCalculator.getChannelBandwidth(), channels, POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
mOutputProcessorFilters.put(channels, taps);
}
return taps;
}
/**
* Internal class for handling requests for start/stop sample stream from polyphase channel sources
*/
@@ -542,9 +458,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
source.dispose();
}
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//ignore
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast so that the tuner source can process this event
mSourceEventBroadcaster.broadcast(sourceEvent);
@@ -560,58 +473,80 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
/**
* Processes the incoming buffer stream from the provider and transfers the buffers to the polyphase channelizer.
*
* This monitor incorporates a queue for receiving source events that affect configuration of the channelizer so
* that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer
* and/or the channel sources and output processors.
* This monitor incorporates a source event handler that queues a center frequency update so that it can be
* handled on the buffer processing thread, avoiding having to lock on the output processor thread. Since we
* anticipate that these two threads will contend for access to this update required flag, we use an update lock
* to protect access to the flag.
*/
public class NativeBufferReceiver implements Listener<INativeBuffer>
{
private Queue<SourceEvent> mQueuedSourceEvents = new ConcurrentLinkedQueue<>();
private boolean mOutputProcessorUpdateRequired = false;
private ReentrantLock mUpdateFlagLock = new ReentrantLock();
/**
* Queues the source event for deferred execution on the buffer processing thread.
* Processes tuner center frequency change source events to flag when output processors need updating.
* @param event that affects configuration of the channelizer (frequency or sample rate change events)
*/
public void receive(SourceEvent event)
{
mQueuedSourceEvents.offer(event);
long frequency = event.getValue().longValue();
if(mChannelCalculator.getCenterFrequency() != frequency)
{
mUpdateFlagLock.lock();
try
{
//Update the channel calculator frequency so that it's ready when the output processor update occurs
mChannelCalculator.setCenterFrequency(frequency);
mOutputProcessorUpdateRequired = true;
}
finally
{
mUpdateFlagLock.unlock();
}
}
}
/**
* Process native buffer streams and update polyphase output channels when the parent tuner center
* frequency changes.
* @param nativeBuffer of sample to process.
*/
@Override
public void receive(INativeBuffer nativeBuffer)
{
mUpdateFlagLock.lock();
try
{
//Process any queued source events before processing the buffers
SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll();
while(queuedSourceEvent != null)
if(mOutputProcessorUpdateRequired)
{
switch(queuedSourceEvent.getEvent())
{
case NOTIFICATION_FREQUENCY_CHANGE:
//Don't send the tuner's frequency change event down to the channels - it would cause chaos
updateOutputProcessors(null);
break;
}
queuedSourceEvent = mQueuedSourceEvents.poll();
updateOutputProcessors();
mOutputProcessorUpdateRequired = false;
}
}
finally
{
mUpdateFlagLock.unlock();
}
if(mPolyphaseChannelizer != null)
if(mPolyphaseChannelizer != null)
{
Iterator<InterleavedComplexSamples> iterator = nativeBuffer.iteratorInterleaved();
while(iterator.hasNext())
{
Iterator<InterleavedComplexSamples> iterator = nativeBuffer.iteratorInterleaved();
while(iterator.hasNext())
try
{
mPolyphaseChannelizer.receive(iterator.next());
}
catch(Throwable throwable)
{
mLog.error("Error", throwable);
}
}
}
catch(Throwable throwable)
{
mLog.error("Error", throwable);
}
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,8 @@
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -26,8 +28,12 @@ import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.StreamProcessorWithHeartbeat;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
/**
* Polyphase Channelizer's Tuner Channel Source implementation. Wraps a ChannelOutputProcessor instance and
@@ -36,34 +42,69 @@ import java.util.List;
*/
public class PolyphaseChannelSource extends TunerChannelSource implements Listener<ComplexSamples>
{
private Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
private IPolyphaseChannelOutputProcessor mReplacementPolyphaseChannelOutputProcessor;
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
private long mReplacementFrequency;
private double mChannelSampleRate;
private long mIndexCenterFrequency;
private long mChannelFrequencyCorrection;
private ReentrantLock mOutputProcessorLock = new ReentrantLock();
private List<Integer> mOutputProcessorIndexes = new ArrayList<>();
private double mTunerSampleRate;
private double mTunerCenterFrequency;
/**
* Constructs an instance
*
* @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate
* @param outputProcessor - to process polyphase channelizer channel results into a channel stream
* @param channelCalculator for current channel center frequency and sample rate and index calculations
* @param filterManager for access to new or cached synthesis filters
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
* @param channelSampleRate for the downstream sample output
* @param centerFrequency of the incoming polyphase channel(s)
* @throws FilterDesignException if a channel low pass filter can't be designed to the channel specification
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
*/
public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
Listener<SourceEvent> producerSourceEventListener, double channelSampleRate,
long centerFrequency) throws FilterDesignException
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
Listener<SourceEvent> producerSourceEventListener) throws IllegalArgumentException
{
super(producerSourceEventListener, tunerChannel);
mPolyphaseChannelOutputProcessor = outputProcessor;
mPolyphaseChannelOutputProcessor.setListener(this);
mChannelSampleRate = channelSampleRate;
mChannelSampleRate = channelCalculator.getChannelSampleRate();
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
setFrequency(centerFrequency);
updateOutputProcessor(channelCalculator, filterManager);
}
/**
* Description of the state or configuration of the output processor.
*/
public String getStateDescription()
{
return mPolyphaseChannelOutputProcessor.getStateDescription();
}
/**
* Current output processor indexes.
* @return indexes
*/
public List<Integer> getOutputProcessorIndexes()
{
return mOutputProcessorIndexes;
}
/**
* Sample rate or bandwidth of the tuner providing input to the channelizer.
* @return sample rate
*/
public double getTunerSampleRate()
{
return mTunerSampleRate;
}
/**
* Center tuned frequency of the tuner providing input to the channelizer.
* @return center frequency
*/
public double getTunerCenterFrequency()
{
return mTunerCenterFrequency;
}
@Override
@@ -109,58 +150,107 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
}
/**
* Channel output processor used by this channel source to convert polyphase channel results into a specific
* channel complex buffer output stream.
* Updates the output processor whenever the source tuner's center frequency changes.
* @param channelCalculator providing access to updated tuner center frequency, sample rate, etc.
* @param filterManager for designing and caching synthesis filters
*/
public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor()
public void updateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager) throws IllegalArgumentException
{
return mPolyphaseChannelOutputProcessor;
}
String errorMessage = null;
/**
* Sets/updates the output processor for this channel source, replacing the existing output processor.
*
* @param outputProcessor to use
* @param frequency center for the channels processed by the output processor
*/
public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency)
{
//If this is the first time, simply assign the output processor
if(mPolyphaseChannelOutputProcessor == null)
mOutputProcessorLock.lock();
try
{
mPolyphaseChannelOutputProcessor = outputProcessor;
mPolyphaseChannelOutputProcessor.setListener(this);
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
//calculator will throw an IllegalArgException ... handled below
List<Integer> indexes = channelCalculator.getChannelIndexes(getTunerChannel());
mOutputProcessorIndexes.clear();
mOutputProcessorIndexes.addAll(indexes);
mTunerCenterFrequency = channelCalculator.getCenterFrequency();
mTunerSampleRate = channelCalculator.getSampleRate();
//The provided channels are necessarily aligned to the center frequency that this source is providing and an
//oscillator will mix the provided channels to bring the desired center frequency to baseband.
setFrequency(channelCalculator.getCenterFrequencyForIndexes(indexes));
if(mPolyphaseChannelOutputProcessor != null && mPolyphaseChannelOutputProcessor.getInputChannelCount() == indexes.size())
{
mPolyphaseChannelOutputProcessor.setPolyphaseChannelIndices(indexes);
if(indexes.size() > 1)
{
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), indexes.size());
mPolyphaseChannelOutputProcessor.setSynthesisFilter(filter);
}
catch(FilterDesignException fde)
{
mLog.error("Error creating an updated synthesis filter for the channel output processor");
errorMessage ="Cannot update output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
}
}
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
else //Create a new output processor.
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.setListener(null);
mPolyphaseChannelOutputProcessor.stop();
}
mPolyphaseChannelOutputProcessor = null;
switch(indexes.size())
{
case 1:
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, channelCalculator.getChannelCount());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
break;
case 2:
try
{
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
channelCalculator.getChannelBandwidth(), 2);
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
indexes, filter, channelCalculator.getChannelCount());
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
mPolyphaseChannelOutputProcessor.start();
}
catch(FilterDesignException fde)
{
errorMessage = "Cannot create new output processor - unable to design synthesis filter for [" +
indexes.size() + "] channel indices";
mLog.error("Error creating a synthesis filter for a new channel output processor");
}
break;
default:
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
mLog.info(channelCalculator.toString());
errorMessage = "Unable to create new channel output processor - unexpected channel index size: " +
indexes.size();
}
}
}
//Otherwise, we have to swap out the processor on the sample processing thread
else
finally
{
mReplacementPolyphaseChannelOutputProcessor = outputProcessor;
mReplacementFrequency = frequency;
mOutputProcessorLock.unlock();
}
}
/**
* Updates the output processor to use the new output processor provided by the external
* polyphase channel manager. This method should only be executed on the sample processing
* thread, within the processChannelResults() method.
*/
private void swapOutputProcessor()
{
if(mReplacementPolyphaseChannelOutputProcessor != null)
//Unlikely, but if we had an error designing a synthesis filter, throw an exception
if(errorMessage != null)
{
IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor;
existingProcessor.stop();
existingProcessor.setListener(null);
//Swap out the processor so that incoming samples can accumulate in the new channel output processor
mPolyphaseChannelOutputProcessor = mReplacementPolyphaseChannelOutputProcessor;
mReplacementPolyphaseChannelOutputProcessor = null;
mPolyphaseChannelOutputProcessor.setListener(this);
mPolyphaseChannelOutputProcessor.start();
//Finally, setup the frequency offset for the output processor.
mIndexCenterFrequency = mReplacementFrequency;
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
throw new IllegalArgumentException(errorMessage);
}
}
@@ -170,15 +260,23 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
* channel aggregation, and dispatch the results to the downstream sample listener/consumer.
*
* @param channelResultsList containing a list of polyphase channelizer output arrays.
* @param currentSamplesTimestamp for the samples
*/
public void receiveChannelResults(List<float[]> channelResultsList)
public void receiveChannelResults(List<float[]> channelResultsList, long currentSamplesTimestamp)
{
if(mReplacementPolyphaseChannelOutputProcessor != null)
{
swapOutputProcessor();
}
mOutputProcessorLock.lock();
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList);
try
{
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList, currentSamplesTimestamp);
}
}
finally
{
mOutputProcessorLock.unlock();
}
}
/**
@@ -212,57 +310,32 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
}
}
@Override
public long getChannelFrequencyCorrection()
{
return mChannelFrequencyCorrection;
}
/**
* Adjusts the frequency correction value that is being applied to the channelized output stream by the
* polyphase channel output processor.
*
* @param value to apply for frequency correction in hertz
*/
protected void setChannelFrequencyCorrection(long value)
{
//TODO: push this frequency correction down to the output processor ...
mChannelFrequencyCorrection = value;
updateFrequencyOffset();
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
}
/**
* Sets the center frequency for this channel.frequency for the incoming sample stream channel results and resets frequency correction to zero.
* Sets the center frequency for this channel.
* @param frequency in hertz
*/
@Override
public void setFrequency(long frequency)
{
mIndexCenterFrequency = frequency;
}
//Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to
//recalculate the frequency error correction again
setChannelFrequencyCorrection(0);
/**
* Center frequency from the incoming index channel(s).
* @return frequency in hertz
*/
public long getIndexCenterFrequency()
{
return mIndexCenterFrequency;
}
/**
* Calculates the frequency offset required to mix the incoming signal to center the desired frequency
* within the channel
*/
private long getFrequencyOffset()
public long getFrequencyOffset()
{
return mIndexCenterFrequency - getTunerChannel().getFrequency() + mChannelFrequencyCorrection;
}
/**
* Updates the frequency offset being applied by the output processor. Calculates the offset using the center
* frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any
* requested frequency correction value.
*/
private void updateFrequencyOffset()
{
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
return mIndexCenterFrequency - getTunerChannel().getFrequency();
}
@Override

View File

@@ -0,0 +1,58 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import java.util.HashMap;
import java.util.Map;
/**
* Creates and caches channel output processor synthesis filters.
*/
public class SynthesisFilterManager
{
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
private static final String SEPARATOR = "-";
private Map<String,float[]> mFilterMap = new HashMap<>();
/**
* Design or retrieve a previously cached output processor synthesis filter.
* @param sampleRate of the tuner
* @param channelBandwidth per channel
* @param channelCount as the number of channels being synthesized/aggregated for the output processor (1 or 2)
* @return filter
* @throws FilterDesignException if the filter cannot be designed based on the supplied parameters.
*/
public float[] getFilter(double sampleRate, double channelBandwidth, int channelCount) throws FilterDesignException
{
String key = sampleRate + SEPARATOR + channelBandwidth + SEPARATOR + channelCount;
if(mFilterMap.containsKey(key))
{
return mFilterMap.get(key);
}
float[] taps = FilterFactory.getSincM2Synthesizer(sampleRate, channelBandwidth, channelCount,
POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
mFilterMap.put(key, taps);
return taps;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,6 +19,7 @@
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
import org.apache.commons.math3.util.FastMath;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
@@ -65,6 +66,20 @@ public class TwoChannelSynthesizerM2
init(filter);
}
/**
* Description of the state/configuration of this synthesizer to support debugging.
* @return state description.
*/
public String getStateDescription()
{
StringBuilder sb = new StringBuilder();
sb.append("Two-Channel Synthesizer");
sb.append("\n\t\t Interleaved Filter: " + Arrays.toString(mIQInterleavedFilter));
sb.append("\n\t\t Serpentine Buffer Length: ").append(mSerpentineDataBuffer.length);
sb.append("\n\t\t Filter Vector Product Length: ").append(mFilterVectorProduct.length);
return sb.toString();
}
/**
* Initializes the synthesizer filter and data buffers for operation.
*
@@ -73,7 +88,6 @@ public class TwoChannelSynthesizerM2
private void init(float[] filter)
{
int tapsPerChannel = (int) FastMath.ceil(filter.length / 2);
mIQInterleavedFilter = getInterleavedFilter(filter, tapsPerChannel);
mSerpentineDataBuffer = new float[mIQInterleavedFilter.length];
mFilterVectorProduct = new float[mIQInterleavedFilter.length];
@@ -156,7 +170,7 @@ public class TwoChannelSynthesizerM2
mTopBlockFlag = !mTopBlockFlag;
}
return new ComplexSamples(i, q);
return new ComplexSamples(i, q, channelBuffer1.timestamp());
}
/**

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