mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-09 15:17:24 -05:00
Compare commits
257 Commits
v0.6.0-alp
...
v0.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db83d5121c | ||
|
|
d61d9c07b0 | ||
|
|
5c9c3fa503 | ||
|
|
552d3a6ce9 | ||
|
|
3bf425f94a | ||
|
|
660a5acd2f | ||
|
|
551277f399 | ||
|
|
a12040be5c | ||
|
|
e4be7dc5f4 | ||
|
|
b615c1e16a | ||
|
|
912af4e3e6 | ||
|
|
df0d0468c8 | ||
|
|
7509245751 | ||
|
|
02e236202c | ||
|
|
4b1d294211 | ||
|
|
40840dbd5c | ||
|
|
786ae9369b | ||
|
|
b7bbf10415 | ||
|
|
486ac01416 | ||
|
|
9e01cc48a5 | ||
|
|
37996d0477 | ||
|
|
a15ff9040b | ||
|
|
b3cd3f431f | ||
|
|
649d1f650a | ||
|
|
215e4064fa | ||
|
|
66a0b2543b | ||
|
|
378841b408 | ||
|
|
e28d726182 | ||
|
|
71bc27e3d9 | ||
|
|
bcfb2700cb | ||
|
|
2d9491ac93 | ||
|
|
4977c3e4fa | ||
|
|
54a0ca5247 | ||
|
|
024a624083 | ||
|
|
9a02416403 | ||
|
|
91e5bc8ba1 | ||
|
|
97afdfa8a9 | ||
|
|
67f91b80f2 | ||
|
|
1b3ce431e6 | ||
|
|
d24895575c | ||
|
|
c97e355349 | ||
|
|
3c8cd0386d | ||
|
|
600db3fb8a | ||
|
|
161595e7c8 | ||
|
|
0ecdf4dd2b | ||
|
|
32ce79b8af | ||
|
|
8070ae3c4b | ||
|
|
55b20c75c0 | ||
|
|
5d4dff7948 | ||
|
|
b1229b6971 | ||
|
|
9474d53a93 | ||
|
|
6f05db723f | ||
|
|
056bccb89a | ||
|
|
e00cd90aef | ||
|
|
8bb59d581f | ||
|
|
b9068da99c | ||
|
|
9f3f7e65a4 | ||
|
|
e6a804836b | ||
|
|
5d8d3e4a41 | ||
|
|
298d9d7016 | ||
|
|
d7fbbcefc5 | ||
|
|
36e198eb1b | ||
|
|
130e59f4c1 | ||
|
|
9223e7c056 | ||
|
|
c3487411fb | ||
|
|
142dd9d579 | ||
|
|
53d537bda8 | ||
|
|
24e11b8ed6 | ||
|
|
14a7b040f9 | ||
|
|
2af3e992a1 | ||
|
|
737c3faacc | ||
|
|
03792b0245 | ||
|
|
dddda052fa | ||
|
|
c50a914632 | ||
|
|
5f082973eb | ||
|
|
2f0419f796 | ||
|
|
cbe451a793 | ||
|
|
aa8d449140 | ||
|
|
7e709d9d6f | ||
|
|
f5938fa8e2 | ||
|
|
ca2509e9f9 | ||
|
|
1d2ba8c06c | ||
|
|
73275aa8ae | ||
|
|
2c32096ec9 | ||
|
|
6532e33296 | ||
|
|
7ea8cb63d2 | ||
|
|
1df29b62eb | ||
|
|
8a5b31f143 | ||
|
|
4d7a8e5980 | ||
|
|
aeeed71231 | ||
|
|
f6c4cc8a5f | ||
|
|
d019388034 | ||
|
|
e125d6a452 | ||
|
|
6b5a1fb827 | ||
|
|
0491b08818 | ||
|
|
7b9adc1da2 | ||
|
|
3acaea0019 | ||
|
|
be02393f65 | ||
|
|
614bf72f01 | ||
|
|
bed28b82ab | ||
|
|
c047d48d91 | ||
|
|
5222bffce4 | ||
|
|
42e7a49d37 | ||
|
|
686bddf7d5 | ||
|
|
1c420b8eee | ||
|
|
e2a0325d07 | ||
|
|
9d03c151af | ||
|
|
3c18bfdc81 | ||
|
|
d89ba65238 | ||
|
|
d2cf2ee7b5 | ||
|
|
7a03331955 | ||
|
|
7c5ef3803a | ||
|
|
6f3e7c86e5 | ||
|
|
fc51de2b4f | ||
|
|
e1e75710cb | ||
|
|
87ce08e9b9 | ||
|
|
1a5c7521a5 | ||
|
|
b54e94dfbe | ||
|
|
19977d5f4d | ||
|
|
0492f19a95 | ||
|
|
098b039154 | ||
|
|
5c6e6059f8 | ||
|
|
72d45543df | ||
|
|
e96fc46315 | ||
|
|
f0cfbb80dd | ||
|
|
a199f03a60 | ||
|
|
526c9609ef | ||
|
|
ec9ceeefb3 | ||
|
|
b6c6505824 | ||
|
|
93c61c48f9 | ||
|
|
9b89e80bb5 | ||
|
|
e0ecaa6e08 | ||
|
|
a7c89b5d1a | ||
|
|
834846d339 | ||
|
|
45c8bb73fa | ||
|
|
d2580f8a99 | ||
|
|
804c2e6359 | ||
|
|
3f2ec612d5 | ||
|
|
8bead998f5 | ||
|
|
2ecdaba183 | ||
|
|
82cb10a03d | ||
|
|
8717ce38eb | ||
|
|
5858a85a50 | ||
|
|
65d16da997 | ||
|
|
b1b5ae5fd2 | ||
|
|
b5ec6d8877 | ||
|
|
8599aebed9 | ||
|
|
2b9e753033 | ||
|
|
dba50744a9 | ||
|
|
490569e023 | ||
|
|
e79f2af759 | ||
|
|
ec7ee1ce91 | ||
|
|
672207c27c | ||
|
|
a1a6b3e988 | ||
|
|
2347135614 | ||
|
|
93438ddd97 | ||
|
|
ae31c41643 | ||
|
|
16aed8741d | ||
|
|
e878ca0c7f | ||
|
|
ca61dd2543 | ||
|
|
6187253589 | ||
|
|
b51c80742f | ||
|
|
15352b9216 | ||
|
|
a848b21ae8 | ||
|
|
279aca7287 | ||
|
|
88879c956a | ||
|
|
729b7e27a8 | ||
|
|
feaed168ae | ||
|
|
50edb18db3 | ||
|
|
7302e2806d | ||
|
|
8845306b4a | ||
|
|
09e19e1486 | ||
|
|
0f5478925c | ||
|
|
09dce0b0d1 | ||
|
|
ad98b1cb5e | ||
|
|
8effd97749 | ||
|
|
9dbed72c32 | ||
|
|
b4dd3692b8 | ||
|
|
030cbb856c | ||
|
|
511101e9b7 | ||
|
|
60dd2b2df7 | ||
|
|
218dc94e02 | ||
|
|
2440f578b1 | ||
|
|
09c87237cb | ||
|
|
3d974ddd91 | ||
|
|
1489c970ba | ||
|
|
9e0afca3d1 | ||
|
|
99eda4013e | ||
|
|
a62f81f890 | ||
|
|
66f0731731 | ||
|
|
dc239801c6 | ||
|
|
d4ba0988bc | ||
|
|
ad0f953b2a | ||
|
|
e1bc9eabd6 | ||
|
|
d3dc1a8667 | ||
|
|
bf278120de | ||
|
|
2f52f6e009 | ||
|
|
02363c3113 | ||
|
|
cfaac0fcbc | ||
|
|
bee0405e00 | ||
|
|
50bdf0ee81 | ||
|
|
a27ddbe0d0 | ||
|
|
0d652b9153 | ||
|
|
9468fd189b | ||
|
|
215737ad9b | ||
|
|
123c25b142 | ||
|
|
af56198f0e | ||
|
|
3c73fdfd6e | ||
|
|
b391ebbb5f | ||
|
|
bad5a1f9f0 | ||
|
|
a7470b463f | ||
|
|
d6c706cb35 | ||
|
|
a0d6950c75 | ||
|
|
2495200b36 | ||
|
|
bfe7ae0b1b | ||
|
|
4912abbf07 | ||
|
|
2f2998d02e | ||
|
|
c96349fa1c | ||
|
|
f640576cc3 | ||
|
|
77d725a488 | ||
|
|
2c0305f928 | ||
|
|
21fd61d507 | ||
|
|
8fb56a5342 | ||
|
|
5d7b73f876 | ||
|
|
85b2a38058 | ||
|
|
c78517ee75 | ||
|
|
ae116d3dfe | ||
|
|
93252f9a40 | ||
|
|
3c03a4db2e | ||
|
|
d0fcd617aa | ||
|
|
6ca0decbd6 | ||
|
|
ddd9ca933e | ||
|
|
b2cc29569e | ||
|
|
411735772b | ||
|
|
2ffb2963af | ||
|
|
227c88c8a2 | ||
|
|
cbe9c79498 | ||
|
|
1b8c100b43 | ||
|
|
30c5fd64f2 | ||
|
|
3a6b486340 | ||
|
|
ded7335dbf | ||
|
|
005a4f8119 | ||
|
|
1ac49bfa7f | ||
|
|
c302c4d121 | ||
|
|
b0a524cf62 | ||
|
|
d246ffff30 | ||
|
|
adfc8b2d6f | ||
|
|
ad2ab08466 | ||
|
|
f3369410b2 | ||
|
|
bac38a5bd8 | ||
|
|
e5f5df74ad | ||
|
|
b3837f581a | ||
|
|
b2ab6f58e9 | ||
|
|
e04b59ba15 | ||
|
|
366eacd234 | ||
|
|
f792876c0a | ||
|
|
19f7a5171b |
10
.github/workflows/gradle.yml
vendored
10
.github/workflows/gradle.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 19
|
||||
uses: actions/setup-java@v2
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up JDK 23
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '19'
|
||||
java-version: '23'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@937999e9cc2425eddc7fd62d1053baf041147db7
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: clean build
|
||||
|
||||
47
.github/workflows/nightly.yml
vendored
Normal file
47
.github/workflows/nightly.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
# Create nightly release for Linux, Mac and Windows using the Linux runner.
|
||||
name: Nightly Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
# Don't run nightly build for pull requests - uncomment to test changes to this action within scope of the PR review
|
||||
# pull_request:
|
||||
# branches: [ master ]
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 23
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '23'
|
||||
distribution: 'liberica'
|
||||
java-package: 'jdk+fx'
|
||||
|
||||
- name: Build Linux and Mac releases with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: runtimeZipOthers -PprojectVersion=nightly
|
||||
|
||||
- name: Build Windows release with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: runtimeZipWindows -PprojectVersion=nightly
|
||||
|
||||
- name: Create Build Info file
|
||||
run: |
|
||||
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
|
||||
|
||||
- name: Add build artifacts to nightly release
|
||||
uses: pyTooling/Actions/releaser@main
|
||||
with:
|
||||
tag: nightly
|
||||
rm: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
files: |
|
||||
build/image/*.zip
|
||||
build/image/build_info.txt
|
||||
1
.idea/dictionaries/denny.xml
generated
1
.idea/dictionaries/denny.xml
generated
@@ -29,6 +29,7 @@
|
||||
<w>talkgroups</w>
|
||||
<w>tgid</w>
|
||||
<w>ultravox</w>
|
||||
<w>unsynchronized</w>
|
||||
<w>wacn</w>
|
||||
<w>xtea</w>
|
||||
</words>
|
||||
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -4,7 +4,7 @@
|
||||
<component name="FrameworkDetectionExcludesConfiguration">
|
||||
<file type="web" url="file://$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_19_PREVIEW" project-jdk-name="19" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_23_PREVIEW" project-jdk-name="liberica-23" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,8 +1,10 @@
|
||||

|
||||

|
||||
|
||||
# sdrtrunk
|
||||
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
|
||||
|
||||
* [Help/Wiki Home Page](https://github.com/DSheirer/sdrtrunk/wiki)
|
||||
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/Getting-Started)
|
||||
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
|
||||
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
|
||||
@@ -18,6 +20,13 @@ All release versions of sdrtrunk are available from the [releases](https://githu
|
||||
* **(beta)** These versions are currently being tested for bugs and functionality prior to final release.
|
||||
* **(final)** These versions have been tested and are the current release version.
|
||||
|
||||
## Download Nightly Software Build
|
||||
The [nightly](https://github.com/DSheirer/sdrtrunk/releases/tag/nightly) release contains current builds of the software
|
||||
for all supported operating systems. This version of the software may contain bugs and may not run correctly. However,
|
||||
it let's you preview the most recent changes and fixes before the next software release. **Always backup your
|
||||
playlist(s) before you use the nightly builds.** Note: the nightly release is updated each time code changes are
|
||||
committed to the code base, so it's not really 'nightly' as much as it is 'current'.
|
||||
|
||||
## Minimum System Requirements
|
||||
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
|
||||
* **CPU:** 4-core
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
Auto-generating Java code from SDRPlay API using jextract.
|
||||
|
||||
Note: the api headers in the 3.11 folder are equivalent to the 3.08 headers. The only
|
||||
difference from the 3.07 headers is the addition of the boolean 'valid' flag to DeviceT structure.
|
||||
|
||||
1. Download JDK19 compatible build of jextract JDK: https://jdk.java.net/jextract/
|
||||
2. If generating on windows platform, modify the sdrplay_api.h to remove the windows
|
||||
pieces of the file. Specifically:
|
||||
2a. Comment out these lines but leave the *HANDLE typedef:
|
||||
|
||||
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
//#include "windows.h"
|
||||
//#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
//#endif
|
||||
|
||||
//#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
//#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
//#define _SDRPLAY_DLL_QUALIFIER
|
||||
//#endif
|
||||
//#endif // _SDRPLAY_DLL_QUALIFIER
|
||||
|
||||
2b. Remove the '_SDRPLAY_DLL_QUALIFIER' from in front of the method declarations
|
||||
|
||||
2c. Change the 3.11 version string to 3.08 either in the header file, or in the generated java class
|
||||
|
||||
3. Open command prompt to the root source code where the generated files should be placed:
|
||||
..\SDRtrunk\sdrplay-api\src\main\java
|
||||
|
||||
4. Generate the code:
|
||||
c:\Users\Denny\Downloads\jdk-19-ea2-panama\bin\jextract "C:\Users\Denny\git\sdrtrunk\sdrplay-api\api\v3_11\sdrplay_api.h" -l libsdrplay_api --source
|
||||
|
||||
5. Do this for all versions of the API.
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
#include "sdrplay_api_rx_channel.h"
|
||||
#include "sdrplay_api_callback.h"
|
||||
|
||||
//#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
//#if defined(_M_X64) || defined(_M_IX86)
|
||||
//#include "windows.h"
|
||||
//#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
//#endif
|
||||
|
||||
//
|
||||
//#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
//#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86))
|
||||
//#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
//#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
//#define _SDRPLAY_DLL_QUALIFIER
|
||||
@@ -21,7 +21,7 @@ typedef void *HANDLE;
|
||||
|
||||
// Application code should check that it is compiled against the same API version
|
||||
// sdrplay_api_ApiVersion() returns the API version
|
||||
#define SDRPLAY_API_VERSION (float)(3.11)
|
||||
#define SDRPLAY_API_VERSION (float)(3.08)
|
||||
|
||||
// API Constants
|
||||
#define SDRPLAY_MAX_DEVICES (16)
|
||||
@@ -198,30 +198,30 @@ extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// Comman API function definitions
|
||||
sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
|
||||
// Device API function definitions
|
||||
sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
// // Comman API function definitions
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
// _SDRPLAY_DLL_QUALIFIER const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
//
|
||||
// // Device API function definitions
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
// _SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
// sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
// sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
78
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_callback.h
Normal file
78
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_callback.h
Normal file
@@ -0,0 +1,78 @@
|
||||
#ifndef SDRPLAY_API_CALLBACK_H
|
||||
#define SDRPLAY_API_CALLBACK_H
|
||||
|
||||
// Event callback enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Overload_Detected = 0,
|
||||
sdrplay_api_Overload_Corrected = 1,
|
||||
} sdrplay_api_PowerOverloadCbEventIdT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_MasterInitialised = 0,
|
||||
sdrplay_api_SlaveAttached = 1,
|
||||
sdrplay_api_SlaveDetached = 2,
|
||||
sdrplay_api_SlaveInitialised = 3,
|
||||
sdrplay_api_SlaveUninitialised = 4,
|
||||
sdrplay_api_MasterDllDisappeared = 5,
|
||||
sdrplay_api_SlaveDllDisappeared = 6,
|
||||
} sdrplay_api_RspDuoModeCbEventIdT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_GainChange = 0,
|
||||
sdrplay_api_PowerOverloadChange = 1,
|
||||
sdrplay_api_DeviceRemoved = 2,
|
||||
sdrplay_api_RspDuoModeChange = 3,
|
||||
} sdrplay_api_EventT;
|
||||
|
||||
// Event callback parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned int gRdB;
|
||||
unsigned int lnaGRdB;
|
||||
double currGain;
|
||||
} sdrplay_api_GainCbParamT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_PowerOverloadCbEventIdT powerOverloadChangeType;
|
||||
} sdrplay_api_PowerOverloadCbParamT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_RspDuoModeCbEventIdT modeChangeType;
|
||||
} sdrplay_api_RspDuoModeCbParamT;
|
||||
|
||||
// Event parameters overlay
|
||||
typedef union
|
||||
{
|
||||
sdrplay_api_GainCbParamT gainParams;
|
||||
sdrplay_api_PowerOverloadCbParamT powerOverloadParams;
|
||||
sdrplay_api_RspDuoModeCbParamT rspDuoModeParams;
|
||||
} sdrplay_api_EventParamsT;
|
||||
|
||||
// Stream callback parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned int firstSampleNum;
|
||||
int grChanged;
|
||||
int rfChanged;
|
||||
int fsChanged;
|
||||
unsigned int numSamples;
|
||||
} sdrplay_api_StreamCbParamsT;
|
||||
|
||||
// Callback function prototypes
|
||||
typedef void (*sdrplay_api_StreamCallback_t)(short *xi, short *xq, sdrplay_api_StreamCbParamsT *params, unsigned int numSamples, unsigned int reset, void *cbContext);
|
||||
typedef void (*sdrplay_api_EventCallback_t)(sdrplay_api_EventT eventId, sdrplay_api_TunerSelectT tuner, sdrplay_api_EventParamsT *params, void *cbContext);
|
||||
|
||||
// Callback function struct
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_StreamCallback_t StreamACbFn;
|
||||
sdrplay_api_StreamCallback_t StreamBCbFn;
|
||||
sdrplay_api_EventCallback_t EventCbFn;
|
||||
} sdrplay_api_CallbackFnsT;
|
||||
|
||||
#endif //SDRPLAY_API_CALLBACK_H
|
||||
45
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_rspDx.h
Normal file
45
artifacts/sdrplay-api-headers/v3_08/sdrplay_api_rspDx.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#ifndef SDRPLAY_API_RSPDX_H
|
||||
#define SDRPLAY_API_RSPDX_H
|
||||
|
||||
#include "sdrplay_api_tuner.h"
|
||||
|
||||
#define RSPDX_NUM_LNA_STATES 28 // Number of LNA states in all bands (except where defined differently below)
|
||||
#define RSPDX_NUM_LNA_STATES_AMPORT2_0_12 19 // Number of LNA states when using AM Port 2 between 0 and 12MHz
|
||||
#define RSPDX_NUM_LNA_STATES_AMPORT2_12_60 20 // Number of LNA states when using AM Port 2 between 12 and 60MHz
|
||||
#define RSPDX_NUM_LNA_STATES_VHF_BAND3 27 // Number of LNA states in VHF and Band3
|
||||
#define RSPDX_NUM_LNA_STATES_420MHZ 21 // Number of LNA states in 420MHz band
|
||||
#define RSPDX_NUM_LNA_STATES_LBAND 19 // Number of LNA states in L-band
|
||||
#define RSPDX_NUM_LNA_STATES_DX 26 // Number of LNA states in DX path
|
||||
|
||||
// RSPdx parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDx_ANTENNA_A = 0,
|
||||
sdrplay_api_RspDx_ANTENNA_B = 1,
|
||||
sdrplay_api_RspDx_ANTENNA_C = 2,
|
||||
} sdrplay_api_RspDx_AntennaSelectT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDx_HDRMODE_BW_0_200 = 0,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_0_500 = 1,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_1_200 = 2,
|
||||
sdrplay_api_RspDx_HDRMODE_BW_1_700 = 3,
|
||||
} sdrplay_api_RspDx_HdrModeBwT;
|
||||
|
||||
// RSPdx parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char hdrEnable; // default: 0
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_RspDx_AntennaSelectT antennaSel; // default: sdrplay_api_RspDx_ANTENNA_A
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
} sdrplay_api_RspDxParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_RspDx_HdrModeBwT hdrBw; // default: sdrplay_api_RspDx_HDRMODE_BW_1_700
|
||||
} sdrplay_api_RspDxTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSPDX_H
|
||||
235
artifacts/sdrplay-api-headers/v3_15/sdrplay_api.h
Normal file
235
artifacts/sdrplay-api-headers/v3_15/sdrplay_api.h
Normal file
@@ -0,0 +1,235 @@
|
||||
#ifndef SDRPLAY_API_H
|
||||
#define SDRPLAY_API_H
|
||||
|
||||
#include "sdrplay_api_dev.h"
|
||||
#include "sdrplay_api_rx_channel.h"
|
||||
#include "sdrplay_api_callback.h"
|
||||
|
||||
#if defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64)
|
||||
#include "windows.h"
|
||||
#elif defined (__GNUC__)
|
||||
typedef void *HANDLE;
|
||||
#endif
|
||||
|
||||
#ifndef _SDRPLAY_DLL_QUALIFIER
|
||||
#if !defined(STATIC_LIB) && (defined(_M_X64) || defined(_M_IX86) || defined(_M_ARM64))
|
||||
#define _SDRPLAY_DLL_QUALIFIER __declspec(dllimport)
|
||||
#elif defined(STATIC_LIB) || defined(__GNUC__)
|
||||
#define _SDRPLAY_DLL_QUALIFIER
|
||||
#endif
|
||||
#endif // _SDRPLAY_DLL_QUALIFIER
|
||||
|
||||
// Application code should check that it is compiled against the same API version
|
||||
// sdrplay_api_ApiVersion() returns the API version
|
||||
#define SDRPLAY_API_VERSION (float)(3.15)
|
||||
|
||||
// API Constants
|
||||
#define SDRPLAY_MAX_DEVICES (16)
|
||||
#define SDRPLAY_MAX_TUNERS_PER_DEVICE (2)
|
||||
|
||||
#define SDRPLAY_MAX_SER_NO_LEN (64)
|
||||
#define SDRPLAY_MAX_ROOT_NM_LEN (32)
|
||||
|
||||
#define SDRPLAY_RSP1_ID (1)
|
||||
#define SDRPLAY_RSP1A_ID (255)
|
||||
#define SDRPLAY_RSP2_ID (2)
|
||||
#define SDRPLAY_RSPduo_ID (3)
|
||||
#define SDRPLAY_RSPdx_ID (4)
|
||||
#define SDRPLAY_RSP1B_ID (6)
|
||||
#define SDRPLAY_RSPdxR2_ID (7)
|
||||
|
||||
// Enum types
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Success = 0,
|
||||
sdrplay_api_Fail = 1,
|
||||
sdrplay_api_InvalidParam = 2,
|
||||
sdrplay_api_OutOfRange = 3,
|
||||
sdrplay_api_GainUpdateError = 4,
|
||||
sdrplay_api_RfUpdateError = 5,
|
||||
sdrplay_api_FsUpdateError = 6,
|
||||
sdrplay_api_HwError = 7,
|
||||
sdrplay_api_AliasingError = 8,
|
||||
sdrplay_api_AlreadyInitialised = 9,
|
||||
sdrplay_api_NotInitialised = 10,
|
||||
sdrplay_api_NotEnabled = 11,
|
||||
sdrplay_api_HwVerError = 12,
|
||||
sdrplay_api_OutOfMemError = 13,
|
||||
sdrplay_api_ServiceNotResponding = 14,
|
||||
sdrplay_api_StartPending = 15,
|
||||
sdrplay_api_StopPending = 16,
|
||||
sdrplay_api_InvalidMode = 17,
|
||||
sdrplay_api_FailedVerification1 = 18,
|
||||
sdrplay_api_FailedVerification2 = 19,
|
||||
sdrplay_api_FailedVerification3 = 20,
|
||||
sdrplay_api_FailedVerification4 = 21,
|
||||
sdrplay_api_FailedVerification5 = 22,
|
||||
sdrplay_api_FailedVerification6 = 23,
|
||||
sdrplay_api_InvalidServiceVersion = 24
|
||||
} sdrplay_api_ErrT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Update_None = 0x00000000,
|
||||
|
||||
// Reasons for master only mode
|
||||
sdrplay_api_Update_Dev_Fs = 0x00000001,
|
||||
sdrplay_api_Update_Dev_Ppm = 0x00000002,
|
||||
sdrplay_api_Update_Dev_SyncUpdate = 0x00000004,
|
||||
sdrplay_api_Update_Dev_ResetFlags = 0x00000008,
|
||||
|
||||
sdrplay_api_Update_Rsp1a_BiasTControl = 0x00000010,
|
||||
sdrplay_api_Update_Rsp1a_RfNotchControl = 0x00000020,
|
||||
sdrplay_api_Update_Rsp1a_RfDabNotchControl = 0x00000040,
|
||||
|
||||
sdrplay_api_Update_Rsp2_BiasTControl = 0x00000080,
|
||||
sdrplay_api_Update_Rsp2_AmPortSelect = 0x00000100,
|
||||
sdrplay_api_Update_Rsp2_AntennaControl = 0x00000200,
|
||||
sdrplay_api_Update_Rsp2_RfNotchControl = 0x00000400,
|
||||
sdrplay_api_Update_Rsp2_ExtRefControl = 0x00000800,
|
||||
|
||||
sdrplay_api_Update_RspDuo_ExtRefControl = 0x00001000,
|
||||
|
||||
sdrplay_api_Update_Master_Spare_1 = 0x00002000,
|
||||
sdrplay_api_Update_Master_Spare_2 = 0x00004000,
|
||||
|
||||
// Reasons for master and slave mode
|
||||
// Note: sdrplay_api_Update_Tuner_Gr MUST be the first value defined in this section!
|
||||
sdrplay_api_Update_Tuner_Gr = 0x00008000,
|
||||
sdrplay_api_Update_Tuner_GrLimits = 0x00010000,
|
||||
sdrplay_api_Update_Tuner_Frf = 0x00020000,
|
||||
sdrplay_api_Update_Tuner_BwType = 0x00040000,
|
||||
sdrplay_api_Update_Tuner_IfType = 0x00080000,
|
||||
sdrplay_api_Update_Tuner_DcOffset = 0x00100000,
|
||||
sdrplay_api_Update_Tuner_LoMode = 0x00200000,
|
||||
|
||||
sdrplay_api_Update_Ctrl_DCoffsetIQimbalance = 0x00400000,
|
||||
sdrplay_api_Update_Ctrl_Decimation = 0x00800000,
|
||||
sdrplay_api_Update_Ctrl_Agc = 0x01000000,
|
||||
sdrplay_api_Update_Ctrl_AdsbMode = 0x02000000,
|
||||
sdrplay_api_Update_Ctrl_OverloadMsgAck = 0x04000000,
|
||||
|
||||
sdrplay_api_Update_RspDuo_BiasTControl = 0x08000000,
|
||||
sdrplay_api_Update_RspDuo_AmPortSelect = 0x10000000,
|
||||
sdrplay_api_Update_RspDuo_Tuner1AmNotchControl = 0x20000000,
|
||||
sdrplay_api_Update_RspDuo_RfNotchControl = 0x40000000,
|
||||
sdrplay_api_Update_RspDuo_RfDabNotchControl = 0x80000000,
|
||||
} sdrplay_api_ReasonForUpdateT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Update_Ext1_None = 0x00000000,
|
||||
|
||||
// Reasons for master only mode
|
||||
sdrplay_api_Update_RspDx_HdrEnable = 0x00000001,
|
||||
sdrplay_api_Update_RspDx_BiasTControl = 0x00000002,
|
||||
sdrplay_api_Update_RspDx_AntennaControl = 0x00000004,
|
||||
sdrplay_api_Update_RspDx_RfNotchControl = 0x00000008,
|
||||
sdrplay_api_Update_RspDx_RfDabNotchControl = 0x00000010,
|
||||
sdrplay_api_Update_RspDx_HdrBw = 0x00000020,
|
||||
sdrplay_api_Update_RspDuo_ResetSlaveFlags = 0x00000040,
|
||||
|
||||
// Reasons for master and slave mode
|
||||
} sdrplay_api_ReasonForUpdateExtension1T;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_DbgLvl_Disable = 0,
|
||||
sdrplay_api_DbgLvl_Verbose = 1,
|
||||
sdrplay_api_DbgLvl_Warning = 2,
|
||||
sdrplay_api_DbgLvl_Error = 3,
|
||||
sdrplay_api_DbgLvl_Message = 4,
|
||||
} sdrplay_api_DbgLvl_t;
|
||||
|
||||
// Device structure
|
||||
typedef struct
|
||||
{
|
||||
char SerNo[SDRPLAY_MAX_SER_NO_LEN];
|
||||
unsigned char hwVer;
|
||||
sdrplay_api_TunerSelectT tuner;
|
||||
sdrplay_api_RspDuoModeT rspDuoMode;
|
||||
unsigned char valid;
|
||||
double rspDuoSampleFreq;
|
||||
HANDLE dev;
|
||||
} sdrplay_api_DeviceT;
|
||||
|
||||
// Device parameter structure
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_DevParamsT *devParams;
|
||||
sdrplay_api_RxChannelParamsT *rxChannelA;
|
||||
sdrplay_api_RxChannelParamsT *rxChannelB;
|
||||
} sdrplay_api_DeviceParamsT;
|
||||
|
||||
// Extended error message structure
|
||||
typedef struct
|
||||
{
|
||||
char file[256];
|
||||
char function[256];
|
||||
int line;
|
||||
char message[1024];
|
||||
} sdrplay_api_ErrorInfoT;
|
||||
|
||||
// Comman API function types
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Open_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Close_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_ApiVersion_t)(float *apiVer);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_LockDeviceApi_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_UnlockDeviceApi_t)(void);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_GetDevices_t)(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SelectDevice_t)(sdrplay_api_DeviceT *device);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_ReleaseDevice_t)(sdrplay_api_DeviceT *device);
|
||||
typedef const char* (*sdrplay_api_GetErrorString_t)(sdrplay_api_ErrT err);
|
||||
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastError_t)(sdrplay_api_DeviceT *device);
|
||||
typedef sdrplay_api_ErrorInfoT* (*sdrplay_api_GetLastErrorByType_t)(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_DisableHeartbeat_t)(void);
|
||||
|
||||
// Device API function types
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_DebugEnable_t)(HANDLE dev, sdrplay_api_DbgLvl_t dbgLvl);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_GetDeviceParams_t)(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Init_t)(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Uninit_t)(HANDLE dev);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_Update_t)(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoActiveTuner_t)(HANDLE dev, sdrplay_api_TunerSelectT *tuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoDualTunerModeSampleRate_t)(double *currentSampleRate, double newSampleRate);
|
||||
typedef sdrplay_api_ErrT (*sdrplay_api_SwapRspDuoMode_t)(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
|
||||
// API function definitions
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
// Comman API function definitions
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Open(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Close(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ApiVersion(float *apiVer);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_LockDeviceApi(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_UnlockDeviceApi(void);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDevices(sdrplay_api_DeviceT *devices, unsigned int *numDevs, unsigned int maxDevs);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SelectDevice(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_ReleaseDevice(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER const char* sdrplay_api_GetErrorString(sdrplay_api_ErrT err);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastError(sdrplay_api_DeviceT *device);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrorInfoT* sdrplay_api_GetLastErrorByType(sdrplay_api_DeviceT *device, int type, unsigned long long *time);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DisableHeartbeat(void); // Must be called before sdrplay_api_SelectDevice()
|
||||
|
||||
// Device API function definitions
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_DebugEnable(HANDLE dev, sdrplay_api_DbgLvl_t enable);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_GetDeviceParams(HANDLE dev, sdrplay_api_DeviceParamsT **deviceParams);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Init(HANDLE dev, sdrplay_api_CallbackFnsT *callbackFns, void *cbContext);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Uninit(HANDLE dev);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_Update(HANDLE dev, sdrplay_api_TunerSelectT tuner, sdrplay_api_ReasonForUpdateT reasonForUpdate, sdrplay_api_ReasonForUpdateExtension1T reasonForUpdateExt1);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoActiveTuner(HANDLE dev, sdrplay_api_TunerSelectT *currentTuner, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoDualTunerModeSampleRate(HANDLE dev, double *currentSampleRate, double newSampleRate);
|
||||
_SDRPLAY_DLL_QUALIFIER sdrplay_api_ErrT sdrplay_api_SwapRspDuoMode(sdrplay_api_DeviceT *currDevice, sdrplay_api_DeviceParamsT **deviceParams,
|
||||
sdrplay_api_RspDuoModeT rspDuoMode, double sampleRate, sdrplay_api_TunerSelectT tuner,
|
||||
sdrplay_api_Bw_MHzT bwType, sdrplay_api_If_kHzT ifType, sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif //SDRPLAY_API_H
|
||||
55
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_control.h
Normal file
55
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_control.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef SDRPLAY_API_CONTROL_H
|
||||
#define SDRPLAY_API_CONTROL_H
|
||||
|
||||
// Control parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_AGC_DISABLE = 0,
|
||||
sdrplay_api_AGC_100HZ = 1,
|
||||
sdrplay_api_AGC_50HZ = 2,
|
||||
sdrplay_api_AGC_5HZ = 3,
|
||||
sdrplay_api_AGC_CTRL_EN = 4
|
||||
} sdrplay_api_AgcControlT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_ADSB_DECIMATION = 0,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_LOWPASS = 1,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_2MHZ = 2,
|
||||
sdrplay_api_ADSB_NO_DECIMATION_BANDPASS_3MHZ = 3
|
||||
} sdrplay_api_AdsbModeT;
|
||||
|
||||
// Control parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char DCenable; // default: 1
|
||||
unsigned char IQenable; // default: 1
|
||||
} sdrplay_api_DcOffsetT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char enable; // default: 0
|
||||
unsigned char decimationFactor; // default: 1
|
||||
unsigned char wideBandSignal; // default: 0
|
||||
} sdrplay_api_DecimationT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_AgcControlT enable; // default: sdrplay_api_AGC_50HZ
|
||||
int setPoint_dBfs; // default: -60
|
||||
unsigned short attack_ms; // default: 0
|
||||
unsigned short decay_ms; // default: 0
|
||||
unsigned short decay_delay_ms; // default: 0
|
||||
unsigned short decay_threshold_dB; // default: 0
|
||||
int syncUpdate; // default: 0
|
||||
} sdrplay_api_AgcT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_DcOffsetT dcOffset;
|
||||
sdrplay_api_DecimationT decimation;
|
||||
sdrplay_api_AgcT agc;
|
||||
sdrplay_api_AdsbModeT adsbMode; //default: sdrplay_api_ADSB_DECIMATION
|
||||
} sdrplay_api_ControlParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_CONTROL_H
|
||||
51
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_dev.h
Normal file
51
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_dev.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#ifndef SDRPLAY_API_DEV_H
|
||||
#define SDRPLAY_API_DEV_H
|
||||
|
||||
#include "sdrplay_api_rsp1a.h"
|
||||
#include "sdrplay_api_rsp2.h"
|
||||
#include "sdrplay_api_rspDuo.h"
|
||||
#include "sdrplay_api_rspDx.h"
|
||||
|
||||
// Dev parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_ISOCH = 0,
|
||||
sdrplay_api_BULK = 1
|
||||
} sdrplay_api_TransferModeT;
|
||||
|
||||
// Dev parameter structs
|
||||
typedef struct
|
||||
{
|
||||
double fsHz; // default: 2000000.0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
unsigned char reCal; // default: 0
|
||||
} sdrplay_api_FsFreqT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned int sampleNum; // default: 0
|
||||
unsigned int period; // default: 0
|
||||
} sdrplay_api_SyncUpdateT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char resetGainUpdate; // default: 0
|
||||
unsigned char resetRfUpdate; // default: 0
|
||||
unsigned char resetFsUpdate; // default: 0
|
||||
} sdrplay_api_ResetFlagsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
double ppm; // default: 0.0
|
||||
sdrplay_api_FsFreqT fsFreq;
|
||||
sdrplay_api_SyncUpdateT syncUpdate;
|
||||
sdrplay_api_ResetFlagsT resetFlags;
|
||||
sdrplay_api_TransferModeT mode; // default: sdrplay_api_ISOCH
|
||||
unsigned int samplesPerPkt; // default: 0 (output param)
|
||||
sdrplay_api_Rsp1aParamsT rsp1aParams;
|
||||
sdrplay_api_Rsp2ParamsT rsp2Params;
|
||||
sdrplay_api_RspDuoParamsT rspDuoParams;
|
||||
sdrplay_api_RspDxParamsT rspDxParams;
|
||||
} sdrplay_api_DevParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_DEV_H
|
||||
22
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp1a.h
Normal file
22
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp1a.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#ifndef SDRPLAY_API_RSP1A_H
|
||||
#define SDRPLAY_API_RSP1A_H
|
||||
|
||||
#define RSPIA_NUM_LNA_STATES 10
|
||||
#define RSPIA_NUM_LNA_STATES_AM 7
|
||||
#define RSPIA_NUM_LNA_STATES_LBAND 9
|
||||
|
||||
// RSP1A parameter enums
|
||||
|
||||
// RSP1A parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
} sdrplay_api_Rsp1aParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
} sdrplay_api_Rsp1aTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSP1A_H
|
||||
35
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp2.h
Normal file
35
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rsp2.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef SDRPLAY_API_RSP2_H
|
||||
#define SDRPLAY_API_RSP2_H
|
||||
|
||||
#define RSPII_NUM_LNA_STATES 9
|
||||
#define RSPII_NUM_LNA_STATES_AMPORT 5
|
||||
#define RSPII_NUM_LNA_STATES_420MHZ 6
|
||||
|
||||
// RSP2 parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Rsp2_ANTENNA_A = 5,
|
||||
sdrplay_api_Rsp2_ANTENNA_B = 6,
|
||||
} sdrplay_api_Rsp2_AntennaSelectT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Rsp2_AMPORT_1 = 1,
|
||||
sdrplay_api_Rsp2_AMPORT_2 = 0,
|
||||
} sdrplay_api_Rsp2_AmPortSelectT;
|
||||
|
||||
// RSP2 parameter structs
|
||||
typedef struct
|
||||
{
|
||||
unsigned char extRefOutputEn; // default: 0
|
||||
} sdrplay_api_Rsp2ParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_Rsp2_AmPortSelectT amPortSel; // default: sdrplay_api_Rsp2_AMPORT_2
|
||||
sdrplay_api_Rsp2_AntennaSelectT antennaSel; // default: sdrplay_api_Rsp2_ANTENNA_A
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
} sdrplay_api_Rsp2TunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSP2_H
|
||||
47
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rspDuo.h
Normal file
47
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rspDuo.h
Normal file
@@ -0,0 +1,47 @@
|
||||
#ifndef SDRPLAY_API_RSPduo_H
|
||||
#define SDRPLAY_API_RSPduo_H
|
||||
|
||||
#define RSPDUO_NUM_LNA_STATES 10
|
||||
#define RSPDUO_NUM_LNA_STATES_AMPORT 5
|
||||
#define RSPDUO_NUM_LNA_STATES_AM 7
|
||||
#define RSPDUO_NUM_LNA_STATES_LBAND 9
|
||||
|
||||
// RSPduo parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDuoMode_Unknown = 0,
|
||||
sdrplay_api_RspDuoMode_Single_Tuner = 1,
|
||||
sdrplay_api_RspDuoMode_Dual_Tuner = 2,
|
||||
sdrplay_api_RspDuoMode_Master = 4,
|
||||
sdrplay_api_RspDuoMode_Slave = 8,
|
||||
} sdrplay_api_RspDuoModeT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_RspDuo_AMPORT_1 = 1,
|
||||
sdrplay_api_RspDuo_AMPORT_2 = 0,
|
||||
} sdrplay_api_RspDuo_AmPortSelectT;
|
||||
|
||||
// RSPduo parameter structs
|
||||
typedef struct
|
||||
{
|
||||
int extRefOutputEn; // default: 0
|
||||
} sdrplay_api_RspDuoParamsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char resetGainUpdate; // default: 0
|
||||
unsigned char resetRfUpdate; // default: 0
|
||||
} sdrplay_api_RspDuo_ResetSlaveFlagsT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char biasTEnable; // default: 0
|
||||
sdrplay_api_RspDuo_AmPortSelectT tuner1AmPortSel; // default: sdrplay_api_RspDuo_AMPORT_2
|
||||
unsigned char tuner1AmNotchEnable; // default: 0
|
||||
unsigned char rfNotchEnable; // default: 0
|
||||
unsigned char rfDabNotchEnable; // default: 0
|
||||
sdrplay_api_RspDuo_ResetSlaveFlagsT resetSlaveFlags;
|
||||
} sdrplay_api_RspDuoTunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RSPduo_H
|
||||
21
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rx_channel.h
Normal file
21
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_rx_channel.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#ifndef SDRPLAY_API_RX_CHANNEL_H
|
||||
#define SDRPLAY_API_RX_CHANNEL_H
|
||||
|
||||
#include "sdrplay_api_tuner.h"
|
||||
#include "sdrplay_api_control.h"
|
||||
#include "sdrplay_api_rsp1a.h"
|
||||
#include "sdrplay_api_rsp2.h"
|
||||
#include "sdrplay_api_rspDuo.h"
|
||||
#include "sdrplay_api_rspDx.h"
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_TunerParamsT tunerParams;
|
||||
sdrplay_api_ControlParamsT ctrlParams;
|
||||
sdrplay_api_Rsp1aTunerParamsT rsp1aTunerParams;
|
||||
sdrplay_api_Rsp2TunerParamsT rsp2TunerParams;
|
||||
sdrplay_api_RspDuoTunerParamsT rspDuoTunerParams;
|
||||
sdrplay_api_RspDxTunerParamsT rspDxTunerParams;
|
||||
} sdrplay_api_RxChannelParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_RX_CHANNEL_H
|
||||
93
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_tuner.h
Normal file
93
artifacts/sdrplay-api-headers/v3_15/sdrplay_api_tuner.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#ifndef SDRPLAY_API_TUNER_H
|
||||
#define SDRPLAY_API_TUNER_H
|
||||
|
||||
#define MAX_BB_GR (59)
|
||||
|
||||
// Tuner parameter enums
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_BW_Undefined = 0,
|
||||
sdrplay_api_BW_0_200 = 200,
|
||||
sdrplay_api_BW_0_300 = 300,
|
||||
sdrplay_api_BW_0_600 = 600,
|
||||
sdrplay_api_BW_1_536 = 1536,
|
||||
sdrplay_api_BW_5_000 = 5000,
|
||||
sdrplay_api_BW_6_000 = 6000,
|
||||
sdrplay_api_BW_7_000 = 7000,
|
||||
sdrplay_api_BW_8_000 = 8000
|
||||
} sdrplay_api_Bw_MHzT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_IF_Undefined = -1,
|
||||
sdrplay_api_IF_Zero = 0,
|
||||
sdrplay_api_IF_0_450 = 450,
|
||||
sdrplay_api_IF_1_620 = 1620,
|
||||
sdrplay_api_IF_2_048 = 2048
|
||||
} sdrplay_api_If_kHzT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_LO_Undefined = 0,
|
||||
sdrplay_api_LO_Auto = 1,
|
||||
sdrplay_api_LO_120MHz = 2,
|
||||
sdrplay_api_LO_144MHz = 3,
|
||||
sdrplay_api_LO_168MHz = 4
|
||||
} sdrplay_api_LoModeT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_EXTENDED_MIN_GR = 0,
|
||||
sdrplay_api_NORMAL_MIN_GR = 20
|
||||
} sdrplay_api_MinGainReductionT;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
sdrplay_api_Tuner_Neither = 0,
|
||||
sdrplay_api_Tuner_A = 1,
|
||||
sdrplay_api_Tuner_B = 2,
|
||||
sdrplay_api_Tuner_Both = 3,
|
||||
} sdrplay_api_TunerSelectT;
|
||||
|
||||
// Tuner parameter structs
|
||||
typedef struct
|
||||
{
|
||||
float curr;
|
||||
float max;
|
||||
float min;
|
||||
} sdrplay_api_GainValuesT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int gRdB; // default: 50
|
||||
unsigned char LNAstate; // default: 0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
sdrplay_api_MinGainReductionT minGr; // default: sdrplay_api_NORMAL_MIN_GR
|
||||
sdrplay_api_GainValuesT gainVals; // output parameter
|
||||
} sdrplay_api_GainT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
double rfHz; // default: 200000000.0
|
||||
unsigned char syncUpdate; // default: 0
|
||||
} sdrplay_api_RfFreqT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
unsigned char dcCal; // default: 3 (Periodic mode)
|
||||
unsigned char speedUp; // default: 0 (No speedup)
|
||||
int trackTime; // default: 1 (=> time in uSec = (72 * 3 * trackTime) / 24e6 = 9uSec)
|
||||
int refreshRateTime; // default: 2048 (=> time in uSec = (72 * 3 * refreshRateTime) / 24e6 = 18432uSec)
|
||||
} sdrplay_api_DcOffsetTunerT;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
sdrplay_api_Bw_MHzT bwType; // default: sdrplay_api_BW_0_200
|
||||
sdrplay_api_If_kHzT ifType; // default: sdrplay_api_IF_Zero
|
||||
sdrplay_api_LoModeT loMode; // default: sdrplay_api_LO_Auto
|
||||
sdrplay_api_GainT gain;
|
||||
sdrplay_api_RfFreqT rfFreq;
|
||||
sdrplay_api_DcOffsetTunerT dcOffsetTuner;
|
||||
} sdrplay_api_TunerParamsT;
|
||||
|
||||
#endif //SDRPLAY_API_TUNER_H
|
||||
99
build.gradle
99
build.gradle
@@ -1,8 +1,6 @@
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,12 +17,16 @@ import java.text.SimpleDateFormat
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Instructions for building/compiling the sdrtrunk application.
|
||||
*
|
||||
* Prerequisites:
|
||||
* Install and configure an OpenJDK version 19+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 7.6+
|
||||
* Install and configure an OpenJDK version 22+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
|
||||
* - Optional: install and configure Gradle 8.10+
|
||||
*
|
||||
* Scenario 1: run the application via gradle command line from the source code root directory:
|
||||
* command: ./gradlew run
|
||||
@@ -53,12 +55,13 @@ repositories {
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
|
||||
version = '0.6.0-alpha6'
|
||||
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
|
||||
version = project.findProperty('projectVersion')
|
||||
|
||||
//Java 19 is required for this version of the Project Panama preview/incubator feature
|
||||
//Java 23 is required for this version of the Project Panama preview/incubator feature
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(19)
|
||||
languageVersion = JavaLanguageVersion.of(23)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +77,11 @@ test {
|
||||
dependencies {
|
||||
|
||||
// JUnit Tests
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.assertj:assertj-core:3.23.1'
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.9.0')
|
||||
testImplementation 'org.mockito:mockito-core:4.8.0'
|
||||
testImplementation(platform("org.junit:junit-bom:5.11.0"))
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0'
|
||||
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
|
||||
|
||||
//Jitpack imports
|
||||
implementation 'com.github.dnault:libresample4j:master'
|
||||
@@ -87,8 +89,8 @@ dependencies {
|
||||
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||
|
||||
//mavenCentral/jcenter imports
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.5'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.5'
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.12'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.14'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
|
||||
implementation 'com.fazecast:jSerialComm:2.9.3'
|
||||
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
@@ -96,16 +98,17 @@ dependencies {
|
||||
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
implementation 'com.github.wendykierp:JTransforms:3.1'
|
||||
implementation 'com.google.code.gson:gson:2.10'
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
implementation 'com.google.guava:guava:33.3.0-jre'
|
||||
implementation 'com.jidesoft:jide-oss:3.6.18'
|
||||
implementation 'com.miglayout:miglayout-swing:11.0'
|
||||
implementation 'com.mpatric:mp3agic:0.9.1'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'eu.hansolo:charts:1.0.5'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
|
||||
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.1' //OSX & Windows aarch64 native libs
|
||||
implementation 'javax.usb:usb-api:1.0.2'
|
||||
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
|
||||
implementation 'org.apache.commons:commons-compress:1.21'
|
||||
implementation 'org.apache.commons:commons-compress:1.26.0'
|
||||
implementation 'org.apache.commons:commons-lang3:3.12.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'org.apache.commons:commons-csv:1.9.0'
|
||||
@@ -117,18 +120,17 @@ dependencies {
|
||||
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java-javax:1.3.0'
|
||||
implementation 'io.github.dsheirer:libusb4java-darwin-aarch64:1.3.1' //usb4java native lib for OSX M1 cpu
|
||||
implementation 'pl.edu.icm:JLargeArrays:1.6'
|
||||
}
|
||||
|
||||
def os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem
|
||||
|
||||
/**
|
||||
* This is needed for the JDK19 panama and vector API ... until it moves out of incubation or preview
|
||||
* This is needed for the JDK23 vector API ... until it moves out of incubation or preview
|
||||
*/
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs.add("--enable-preview") //Panama Foreign Function/Memory is preview in JDK 19
|
||||
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Needed while Panama Vector API remains in incubator
|
||||
options.compilerArgs.add("--enable-preview") //Project Panama Vector API is still in incubator
|
||||
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Project Panama Vector API is still in incubator
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,10 +139,10 @@ tasks.withType(JavaCompile) {
|
||||
def jvmArgsWindows =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED', //windows users for JIDE
|
||||
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
|
||||
'--enable-native-access=ALL-UNNAMED',
|
||||
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //Allows Project Panama Foreign Function access to system library
|
||||
'--add-modules=jdk.incubator.vector', //Project Panama Vector API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector API is still in incubator
|
||||
'--enable-native-access=ALL-UNNAMED', //Suppress unsafe warnings for FFM API Foreign Memory calls
|
||||
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //SDRPlay API library path
|
||||
|
||||
/**
|
||||
* JVM Arguments for running the application on Linux.
|
||||
@@ -149,8 +151,7 @@ def jvmArgsLinux =
|
||||
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
|
||||
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
|
||||
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
|
||||
'--enable-native-access=ALL-UNNAMED',
|
||||
'-Djava.library.path=/usr/local/lib'] //Allows Project Panama Foreign Function access to system library
|
||||
'--enable-native-access=ALL-UNNAMED'] //SDRPlay API library path
|
||||
|
||||
application {
|
||||
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
|
||||
@@ -191,28 +192,31 @@ String targetLinuxX86_64 = 'linux-x86_64-v' + version
|
||||
String targetOsxAarch64 = 'osx-aarch64-v' + version
|
||||
String targetOsxX86_64 = 'osx-x86_64-v' + version
|
||||
String targetWindowsX86_64 = 'windows-x86_64-v' + version
|
||||
String targetWindowsAarch64 = 'windows-aarch64-v' + version
|
||||
|
||||
/**
|
||||
* Optional pre-downloaded Java Development Kit (JDK) for each target OS and CPU. These locations are hard-coded to the
|
||||
* author's development environment, but can be changed to your environment if you want to use this build pattern.
|
||||
*/
|
||||
String jdk_base = '/home/denny/java_jdks/'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-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'
|
||||
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-22.0.2-full'
|
||||
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-22.0.2-full'
|
||||
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-22.0.2-full.jdk'
|
||||
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-22.0.2-full.jdk'
|
||||
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-22.0.2-full'
|
||||
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-22.0.2-full'
|
||||
|
||||
/**
|
||||
* Download URLs to download the JDK as part of the gradle build packaging process
|
||||
*/
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/19.0.1+11/bellsoft-jdk19.0.1+11-"
|
||||
def jdk_download_base = "https://download.bell-sw.com/java/23.0.1+13/bellsoft-jdk23.0.1+13-"
|
||||
def jdk_download_suffix = "-full.tar.gz"
|
||||
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
|
||||
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_x86_64 = jdk_download_base + "macos-amd64" + jdk_download_suffix
|
||||
def jdk_download_osx_aarch64 = jdk_download_base + "macos-aarch64" + jdk_download_suffix
|
||||
def jdk_download_windows_x86_64 = jdk_download_base + "windows-amd64-full.zip"
|
||||
def jdk_download_windows_aarch64 = jdk_download_base + "windows-aarch64-full.zip"
|
||||
|
||||
/**
|
||||
* Configures the runtime zip task with additional options/settings.
|
||||
@@ -228,7 +232,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
|
||||
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
|
||||
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
|
||||
//jdk.accessibility is used with assistive technologies like screen readers
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility')
|
||||
//java.management for JVM resource monitoring
|
||||
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
|
||||
|
||||
//Use auto-detected modules and 'add' any specified modules.
|
||||
rt.extension.additive.set(true)
|
||||
@@ -254,7 +259,12 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
|
||||
|
||||
if(os.isWindows()) {
|
||||
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
|
||||
if(arch.equals("amd64")) {
|
||||
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
|
||||
}
|
||||
else if(arch.equals("aarch64")) {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64, javaHome)
|
||||
}
|
||||
configure(rt, jvmArgsWindows)
|
||||
}
|
||||
else if(os.isLinux())
|
||||
@@ -290,6 +300,16 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
|
||||
*/
|
||||
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
//Windows aarch64
|
||||
if(file(jdk_windows_aarch64).exists()) {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, [])
|
||||
}
|
||||
else {
|
||||
rt.extension.targetPlatform(targetWindowsAarch64) {
|
||||
jdkHome = jdkDownload(jdk_download_windows_aarch64)
|
||||
}
|
||||
}
|
||||
//Windows x86-64
|
||||
if(file(jdk_windows_x86_64).exists()) {
|
||||
rt.extension.targetPlatform(targetWindowsX86_64, jdk_windows_x86_64, [])
|
||||
}
|
||||
@@ -339,7 +359,7 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
jdkHome = jdkDownload(jdk_download_osx_aarch64)
|
||||
}
|
||||
}
|
||||
//Linux x86-64
|
||||
//OSX x86-64
|
||||
if(file(jdk_osx_x86_64).exists()) {
|
||||
rt.extension.targetPlatform(targetOsxX86_64, jdk_osx_x86_64, [])
|
||||
}
|
||||
@@ -351,4 +371,3 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
|
||||
|
||||
configure(rt, jvmArgsLinux);
|
||||
}
|
||||
|
||||
|
||||
31
gradle.properties
Normal file
31
gradle.properties
Normal file
@@ -0,0 +1,31 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
# *****************************************************************************
|
||||
#
|
||||
|
||||
# Version naming conventions:
|
||||
# Alpha: maj.min.patch-alpha-build
|
||||
# Beta: maj.min.patch-beta-build
|
||||
# Release: maj.min.patch
|
||||
#
|
||||
# Example: 0.6.0-beta-5
|
||||
#
|
||||
# Note: this is so that the file names for the resultant release build products match the GitLab release asset
|
||||
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
|
||||
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
|
||||
|
||||
projectVersion=0.6.1
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
5
gradle/wrapper/gradle-wrapper.properties
vendored
5
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2022 Dennis Sheirer
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -19,7 +19,8 @@
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
18
gradlew
vendored
18
gradlew
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
#
|
||||
# ******************************************************************************
|
||||
# Copyright (C) 2014-2022 Dennis Sheirer
|
||||
# Copyright (C) 2014-2024 Dennis Sheirer
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -88,9 +88,6 @@ done
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
@@ -136,10 +133,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -147,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -155,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -200,6 +200,10 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,37 +19,31 @@
|
||||
package io.github.dsheirer.alias;
|
||||
|
||||
import io.github.dsheirer.alias.action.AliasAction;
|
||||
import io.github.dsheirer.alias.action.AliasActionType;
|
||||
import io.github.dsheirer.alias.action.beep.BeepAction;
|
||||
import io.github.dsheirer.alias.action.clip.ClipAction;
|
||||
import io.github.dsheirer.alias.action.script.ScriptAction;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
|
||||
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
|
||||
import io.github.dsheirer.alias.id.legacy.mobileID.Min;
|
||||
import io.github.dsheirer.alias.id.legacy.mpt1327.MPT1327ID;
|
||||
import io.github.dsheirer.alias.id.legacy.nonrecordable.NonRecordable;
|
||||
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
|
||||
import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class AliasFactory
|
||||
{
|
||||
@@ -64,6 +58,11 @@ public class AliasFactory
|
||||
BroadcastChannel copyBroadcast = new BroadcastChannel();
|
||||
copyBroadcast.setChannelName(originalBroadcast.getChannelName());
|
||||
return copyBroadcast;
|
||||
case DCS:
|
||||
Dcs originalDcs = (Dcs)id;
|
||||
Dcs copyDcs = new Dcs();
|
||||
copyDcs.setDCSCode(originalDcs.getDCSCode());
|
||||
return copyDcs;
|
||||
case ESN:
|
||||
Esn originalESN = (Esn)id;
|
||||
Esn copyESN = new Esn();
|
||||
@@ -80,6 +79,18 @@ public class AliasFactory
|
||||
Min copyMin = new Min();
|
||||
copyMin.setMin(originalMin.getMin());
|
||||
return copyMin;
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
|
||||
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),
|
||||
originalP25.getSystem(), originalP25.getValue());
|
||||
copyP25.setOverlap(originalP25.overlapProperty().get());
|
||||
return copyP25;
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
P25FullyQualifiedTalkgroup originalFqt = (P25FullyQualifiedTalkgroup) id;
|
||||
P25FullyQualifiedTalkgroup copyFqt = new P25FullyQualifiedTalkgroup(originalFqt.getWacn(),
|
||||
originalFqt.getSystem(), originalFqt.getValue());
|
||||
copyFqt.setOverlap(originalFqt.overlapProperty().get());
|
||||
return copyFqt;
|
||||
case PRIORITY:
|
||||
Priority originalPriority = (Priority)id;
|
||||
Priority copyPriority = new Priority();
|
||||
@@ -187,6 +198,7 @@ public class AliasFactory
|
||||
copy.setGroup(original.getGroup());
|
||||
copy.setColor(original.getColor());
|
||||
copy.setIconName(original.getIconName());
|
||||
copy.setRecordable(original.isRecordable());
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -234,65 +246,4 @@ public class AliasFactory
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
public static AliasID getAliasID(AliasIDType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case BROADCAST_CHANNEL:
|
||||
return new BroadcastChannel();
|
||||
case ESN:
|
||||
return new Esn();
|
||||
case FLEETSYNC:
|
||||
return new FleetsyncID();
|
||||
case LTR_NET_UID:
|
||||
return new UniqueID();
|
||||
case LOJACK:
|
||||
return new LoJackFunctionAndID();
|
||||
case MDC1200:
|
||||
return new MDC1200ID();
|
||||
case MIN:
|
||||
return new Min();
|
||||
case MPT1327:
|
||||
return new MPT1327ID();
|
||||
case NON_RECORDABLE:
|
||||
return new NonRecordable();
|
||||
case PRIORITY:
|
||||
return new Priority();
|
||||
case RADIO_ID:
|
||||
return new Radio();
|
||||
case RADIO_ID_RANGE:
|
||||
return new RadioRange();
|
||||
case RECORD:
|
||||
return new Record();
|
||||
case TALKGROUP:
|
||||
return new Talkgroup();
|
||||
case TALKGROUP_RANGE:
|
||||
return new TalkgroupRange();
|
||||
case SITE:
|
||||
return new SiteID();
|
||||
case STATUS:
|
||||
return new UserStatusID();
|
||||
case LEGACY_TALKGROUP:
|
||||
return new LegacyTalkgroupID();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized Alias ID type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
public static AliasAction getAliasAction(AliasActionType type)
|
||||
{
|
||||
switch(type)
|
||||
{
|
||||
case BEEP:
|
||||
return new BeepAction();
|
||||
case CLIP:
|
||||
return new ClipAction();
|
||||
case SCRIPT:
|
||||
return new ScriptAction();
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized Alias Action type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -21,32 +21,34 @@ package io.github.dsheirer.alias;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.dcs.DCSIdentifier;
|
||||
import io.github.dsheirer.identifier.esn.ESNIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.FullyQualifiedRadioIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.status.UnitStatusIdentifier;
|
||||
import io.github.dsheirer.identifier.status.UserStatusIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -57,6 +59,10 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* List of aliases that share the same alias list name and provides convenient methods for looking up alias
|
||||
@@ -67,6 +73,7 @@ public class AliasList
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AliasList.class);
|
||||
private Map<Protocol,TalkgroupAliasList> mTalkgroupProtocolMap = new EnumMap<>(Protocol.class);
|
||||
private Map<Protocol,RadioAliasList> mRadioProtocolMap = new EnumMap<>(Protocol.class);
|
||||
private Map<DCSCode,Alias> mDCSCodeAliasMap = new EnumMap<>(DCSCode.class);
|
||||
private Map<String,Alias> mESNMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mUnitStatusMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mUserStatusMap = new HashMap<>();
|
||||
@@ -152,6 +159,32 @@ public class AliasList
|
||||
|
||||
talkgroupRangeAliasList.add(talkgroupRange, alias);
|
||||
break;
|
||||
case P25_FULLY_QUALIFIED_RADIO_ID:
|
||||
P25FullyQualifiedRadio qualifiedRadio = (P25FullyQualifiedRadio) id;
|
||||
|
||||
RadioAliasList p25RadioAliasList = mRadioProtocolMap.get(qualifiedRadio.getProtocol());
|
||||
|
||||
if(p25RadioAliasList == null)
|
||||
{
|
||||
p25RadioAliasList = new RadioAliasList();
|
||||
mRadioProtocolMap.put(qualifiedRadio.getProtocol(), p25RadioAliasList);
|
||||
}
|
||||
|
||||
p25RadioAliasList.add(qualifiedRadio, alias);
|
||||
break;
|
||||
case P25_FULLY_QUALIFIED_TALKGROUP:
|
||||
P25FullyQualifiedTalkgroup qualifiedTalkgroup = (P25FullyQualifiedTalkgroup) id;
|
||||
|
||||
TalkgroupAliasList p25TalkgroupAliasList = mTalkgroupProtocolMap.get(qualifiedTalkgroup.getProtocol());
|
||||
|
||||
if(p25TalkgroupAliasList == null)
|
||||
{
|
||||
p25TalkgroupAliasList = new TalkgroupAliasList();
|
||||
mTalkgroupProtocolMap.put(qualifiedTalkgroup.getProtocol(), p25TalkgroupAliasList);
|
||||
}
|
||||
|
||||
p25TalkgroupAliasList.add(qualifiedTalkgroup, alias);
|
||||
break;
|
||||
case RADIO_ID:
|
||||
Radio radio = (Radio)id;
|
||||
|
||||
@@ -178,6 +211,12 @@ public class AliasList
|
||||
|
||||
radioRangeAliasList.add(radioRange, alias);
|
||||
break;
|
||||
case DCS:
|
||||
if(id instanceof Dcs dcs)
|
||||
{
|
||||
mDCSCodeAliasMap.put(dcs.getDCSCode(), alias);
|
||||
}
|
||||
break;
|
||||
case ESN:
|
||||
String esn = ((Esn)id).getEsn();
|
||||
|
||||
@@ -379,6 +418,8 @@ public class AliasList
|
||||
}
|
||||
break;
|
||||
case PATCH_GROUP:
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
PatchGroupIdentifier patchGroupIdentifier = (PatchGroupIdentifier)identifier;
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
@@ -386,8 +427,6 @@ public class AliasList
|
||||
|
||||
if(patchGroupAliasList != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
|
||||
Alias alias = patchGroupAliasList.getAlias(patchGroup.getPatchGroup());
|
||||
|
||||
if(alias != null)
|
||||
@@ -395,19 +434,36 @@ public class AliasList
|
||||
aliases.add(alias);
|
||||
}
|
||||
|
||||
for(TalkgroupIdentifier patchedGroup: patchGroup.getPatchedGroupIdentifiers())
|
||||
for(TalkgroupIdentifier patchedTalkgroup: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
Alias patchedAlias = patchGroupAliasList.getAlias(patchedGroup);
|
||||
Alias patchedTalkgroupAlias = patchGroupAliasList.getAlias(patchedTalkgroup);
|
||||
|
||||
if(patchedAlias != null && !aliases.contains(patchedAlias))
|
||||
if(patchedTalkgroupAlias != null && !aliases.contains(patchedTalkgroupAlias))
|
||||
{
|
||||
aliases.add(patchedAlias);
|
||||
aliases.add(patchedTalkgroupAlias);
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
}
|
||||
break;
|
||||
|
||||
if(patchGroup.hasPatchedRadios())
|
||||
{
|
||||
RadioAliasList radioAliasList = mRadioProtocolMap.get(patchGroupIdentifier.getProtocol());
|
||||
|
||||
if(radioAliasList != null)
|
||||
{
|
||||
for(RadioIdentifier patchedRadio: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
Alias patchedRadioAlias = radioAliasList.getAlias(patchedRadio);
|
||||
|
||||
if(patchedRadioAlias != null && !aliases.contains(patchedRadioAlias))
|
||||
{
|
||||
aliases.add(patchedRadioAlias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return aliases;
|
||||
case RADIO:
|
||||
RadioIdentifier radio = (RadioIdentifier)identifier;
|
||||
|
||||
@@ -439,9 +495,9 @@ public class AliasList
|
||||
}
|
||||
break;
|
||||
case TONE:
|
||||
if(identifier instanceof ToneIdentifier)
|
||||
if(identifier instanceof ToneIdentifier toneIdentifier)
|
||||
{
|
||||
ToneSequence toneSequence = ((ToneIdentifier)identifier).getValue();
|
||||
ToneSequence toneSequence = toneIdentifier.getValue();
|
||||
|
||||
if(toneSequence != null && toneSequence.hasTones())
|
||||
{
|
||||
@@ -454,6 +510,15 @@ public class AliasList
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier instanceof DCSIdentifier dcsIdentifier)
|
||||
{
|
||||
DCSCode dcsCode = dcsIdentifier.getValue();
|
||||
|
||||
if(dcsCode != null)
|
||||
{
|
||||
return toList(mDCSCodeAliasMap.get(dcsCode));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -465,9 +530,7 @@ public class AliasList
|
||||
{
|
||||
if(alias != null)
|
||||
{
|
||||
List<Alias> aliases = new ArrayList<>();
|
||||
aliases.add(alias);
|
||||
return aliases;
|
||||
return Collections.singletonList(alias);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
@@ -590,6 +653,7 @@ public class AliasList
|
||||
*/
|
||||
public class TalkgroupAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedTalkgroupAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mTalkgroupAliasMap = new TreeMap<>();
|
||||
private Map<TalkgroupRange, Alias> mTalkgroupRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -599,6 +663,13 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(TalkgroupIdentifier identifier)
|
||||
{
|
||||
//Attempt to do a fully qualified identifier match only
|
||||
if(identifier instanceof FullyQualifiedTalkgroupIdentifier fqti)
|
||||
{
|
||||
return mFullyQualifiedTalkgroupAliasMap.get(fqti.getFullyQualifiedTalkgroupAddress());
|
||||
}
|
||||
|
||||
//Attempt to match the talkgroup value
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mTalkgroupAliasMap.get(value);
|
||||
@@ -607,6 +678,7 @@ public class AliasList
|
||||
return mapValue;
|
||||
}
|
||||
|
||||
//Alternatively, match the talkgroup to any talkgroup ranges
|
||||
for(Map.Entry<TalkgroupRange, Alias> entry : mTalkgroupRangeAliasMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().contains(value))
|
||||
@@ -620,26 +692,55 @@ public class AliasList
|
||||
|
||||
public void add(Talkgroup talkgroup, Alias alias)
|
||||
{
|
||||
//Detect talkgroup collisions and set overlap flag for both
|
||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||
if(talkgroup instanceof P25FullyQualifiedTalkgroup fqt)
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedTalkgroupAliasMap.containsKey(fqt.getHashKey()))
|
||||
{
|
||||
talkgroup.setOverlap(true);
|
||||
Alias existing = mFullyQualifiedTalkgroupAliasMap.get(fqt.getHashKey());
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
||||
fqt.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
if(aliasID instanceof P25FullyQualifiedTalkgroup existingFqt &&
|
||||
existingFqt.getHashKey().contentEquals(fqt.getHashKey()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mFullyQualifiedTalkgroupAliasMap.put(fqt.getHashKey(), alias);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Detect talkgroup collisions and set overlap flag for both
|
||||
if(mTalkgroupAliasMap.containsKey(talkgroup.getValue()))
|
||||
{
|
||||
Alias existing = mTalkgroupAliasMap.get(talkgroup.getValue());
|
||||
|
||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
talkgroup.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
if(aliasID instanceof Talkgroup && ((Talkgroup)aliasID).getValue() == talkgroup.getValue())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mTalkgroupAliasMap.put(talkgroup.getValue(), alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(TalkgroupRange talkgroupRange, Alias alias)
|
||||
@@ -672,6 +773,7 @@ public class AliasList
|
||||
*/
|
||||
public class RadioAliasList
|
||||
{
|
||||
private Map<String,Alias> mFullyQualifiedRadioAliasMap = new HashMap<>();
|
||||
private Map<Integer,Alias> mRadioAliasMap = new TreeMap<>();
|
||||
private Map<RadioRange, Alias> mRadioRangeAliasMap = new HashMap<>();
|
||||
|
||||
@@ -681,6 +783,13 @@ public class AliasList
|
||||
|
||||
public Alias getAlias(RadioIdentifier identifier)
|
||||
{
|
||||
//Match fully qualified identifier only.
|
||||
if(identifier instanceof FullyQualifiedRadioIdentifier fqri)
|
||||
{
|
||||
return mFullyQualifiedRadioAliasMap.get(fqri.getFullyQualifiedRadioAddress());
|
||||
}
|
||||
|
||||
//Attempt to match against the radio identifier
|
||||
int value = identifier.getValue();
|
||||
|
||||
Alias mapValue = mRadioAliasMap.get(value);
|
||||
@@ -689,6 +798,7 @@ public class AliasList
|
||||
return mapValue;
|
||||
}
|
||||
|
||||
//Alternatively, attempt to match the radio address against any radio ranges.
|
||||
for(Map.Entry<RadioRange, Alias> entry : mRadioRangeAliasMap.entrySet())
|
||||
{
|
||||
if(entry.getKey().contains(value))
|
||||
@@ -702,26 +812,55 @@ public class AliasList
|
||||
|
||||
public void add(Radio radio, Alias alias)
|
||||
{
|
||||
//Detect collisions
|
||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||
if(radio instanceof P25FullyQualifiedRadio fqr)
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
if(!existing.equals(alias))
|
||||
//Detect collisions
|
||||
if(mFullyQualifiedRadioAliasMap.containsKey(fqr.getHashKey()))
|
||||
{
|
||||
radio.setOverlap(true);
|
||||
Alias existing = mFullyQualifiedRadioAliasMap.get(fqr.getHashKey());
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
if(aliasID instanceof Radio && ((Radio)aliasID).getValue() == radio.getValue())
|
||||
fqr.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
if(aliasID instanceof P25FullyQualifiedRadio existingFqr &&
|
||||
existingFqr.getHashKey().contentEquals(fqr.getHashKey()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mFullyQualifiedRadioAliasMap.put(fqr.getHashKey(), alias);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Detect collisions
|
||||
if(mRadioAliasMap.containsKey(radio.getValue()))
|
||||
{
|
||||
Alias existing = mRadioAliasMap.get(radio.getValue());
|
||||
|
||||
mRadioAliasMap.put(radio.getValue(), alias);
|
||||
if(!existing.equals(alias))
|
||||
{
|
||||
radio.setOverlap(true);
|
||||
|
||||
for(AliasID aliasID: existing.getAliasIdentifiers())
|
||||
{
|
||||
if(aliasID instanceof Radio existingRadio && (existingRadio.getValue() == radio.getValue()))
|
||||
{
|
||||
aliasID.setOverlap(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mRadioAliasMap.put(radio.getValue(), alias);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(RadioRange radioRange, Alias alias)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -63,6 +63,69 @@ public class AliasModel
|
||||
return mAliasListNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears and reloads the list of alias list names from the current set of aliases.
|
||||
*/
|
||||
public void refreshAliasListNames()
|
||||
{
|
||||
mAliasListNames.clear();
|
||||
|
||||
for(Alias alias : mAliases)
|
||||
{
|
||||
String aliasListName = alias.getAliasListName();
|
||||
|
||||
if(aliasListName != null && !aliasListName.isEmpty() && !mAliasListNames.contains(aliasListName))
|
||||
{
|
||||
mAliasListNames.add(aliasListName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the alias list across the set of aliases.
|
||||
* @param oldName currently used by the alias
|
||||
* @param newName to apply to the alias
|
||||
*/
|
||||
public void renameAliasList(String oldName, String newName)
|
||||
{
|
||||
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mAliases.stream().filter(alias -> alias.getAliasListName().equals(oldName)).forEach(alias -> alias.setAliasListName(newName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes any aliases that have the alias list name
|
||||
* @param aliasListName to delete
|
||||
*/
|
||||
public void deleteAliasList(String aliasListName)
|
||||
{
|
||||
if(aliasListName == null || aliasListName.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mAliases.removeIf(alias -> alias.getAliasListName().equals(aliasListName));
|
||||
mAliasListMap.remove(aliasListName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the list of alias list names to the currently managed set of alias list names.
|
||||
* @param aliasListNames to add
|
||||
*/
|
||||
public void addAliasListNames(List<String> aliasListNames)
|
||||
{
|
||||
for(String aliasListName : aliasListNames)
|
||||
{
|
||||
if(!mAliasListNames.contains(aliasListName))
|
||||
{
|
||||
mAliasListNames.add(aliasListName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmodifiable list of all aliases currently in the model
|
||||
*/
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.alias.id;
|
||||
|
||||
@@ -26,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.alias.id.dcs.Dcs;
|
||||
import io.github.dsheirer.alias.id.esn.Esn;
|
||||
import io.github.dsheirer.alias.id.legacy.fleetsync.FleetsyncID;
|
||||
import io.github.dsheirer.alias.id.legacy.mdc.MDC1200ID;
|
||||
@@ -37,11 +35,13 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
|
||||
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
|
||||
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
|
||||
import io.github.dsheirer.alias.id.priority.Priority;
|
||||
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
|
||||
import io.github.dsheirer.alias.id.radio.Radio;
|
||||
import io.github.dsheirer.alias.id.radio.RadioRange;
|
||||
import io.github.dsheirer.alias.id.record.Record;
|
||||
import io.github.dsheirer.alias.id.status.UnitStatusID;
|
||||
import io.github.dsheirer.alias.id.status.UserStatusID;
|
||||
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
|
||||
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
|
||||
import io.github.dsheirer.alias.id.tone.TonesID;
|
||||
@@ -54,6 +54,7 @@ import javafx.util.Callback;
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = BroadcastChannel.class, name = "broadcastChannel"),
|
||||
@JsonSubTypes.Type(value = Dcs.class, name = "dcs"),
|
||||
@JsonSubTypes.Type(value = Esn.class, name = "esn"),
|
||||
@JsonSubTypes.Type(value = FleetsyncID.class, name = "fleetsyncID"),
|
||||
@JsonSubTypes.Type(value = LegacyTalkgroupID.class, name = "talkgroupID"),
|
||||
@@ -62,6 +63,8 @@ import javafx.util.Callback;
|
||||
@JsonSubTypes.Type(value = Min.class, name = "min"),
|
||||
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
|
||||
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
|
||||
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
|
||||
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
|
||||
@JsonSubTypes.Type(value = Radio.class, name = "radio"),
|
||||
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.alias.id;
|
||||
|
||||
@@ -26,11 +23,14 @@ import java.util.EnumSet;
|
||||
public enum AliasIDType
|
||||
{
|
||||
BROADCAST_CHANNEL("Audio Broadcast Channel"),
|
||||
DCS("Digital Coded Squelch (DCS)"),
|
||||
ESN("ESN"),
|
||||
INVERT("Audio Inversion"),
|
||||
LOJACK("LoJack"),
|
||||
LTR_NET_UID("LTR-Net UID"),
|
||||
MIN("Passport MIN"),
|
||||
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
|
||||
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
|
||||
PRIORITY("Audio Priority"),
|
||||
RADIO_ID("Radio ID"),
|
||||
RADIO_ID_RANGE("Radio ID Range"),
|
||||
@@ -58,7 +58,7 @@ public enum AliasIDType
|
||||
|
||||
//Values used by the View-By alias editor
|
||||
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
|
||||
UNIT_STATUS, STATUS, TONES);
|
||||
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
|
||||
|
||||
public String toString()
|
||||
{
|
||||
|
||||
111
src/main/java/io/github/dsheirer/alias/id/dcs/Dcs.java
Normal file
111
src/main/java/io/github/dsheirer/alias/id/dcs/Dcs.java
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.dcs;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasID;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.module.decode.dcs.DCSCode;
|
||||
|
||||
/**
|
||||
* Digital Coded Squelch (DCS) tone identifier
|
||||
*/
|
||||
public class Dcs extends AliasID implements Comparable<Dcs>
|
||||
{
|
||||
private DCSCode mDCSCode;
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.DCS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(AliasID id)
|
||||
{
|
||||
if(isValid() && id instanceof Dcs other)
|
||||
{
|
||||
return other.isValid() && getDCSCode().equals(other.getDCSCode());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* DCS code
|
||||
* @return DCS code or null.
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "code")
|
||||
public DCSCode getDCSCode()
|
||||
{
|
||||
return mDCSCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the DCS code value.
|
||||
* @param dcsCode to set
|
||||
*/
|
||||
public void setDCSCode(DCSCode dcsCode)
|
||||
{
|
||||
mDCSCode = dcsCode;
|
||||
updateValueProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return mDCSCode != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAudioIdentifier()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
if(isValid())
|
||||
{
|
||||
return getDCSCode().toString();
|
||||
}
|
||||
|
||||
return "DCS-Invalid - No Tone Selected";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Dcs o)
|
||||
{
|
||||
if(isValid())
|
||||
{
|
||||
if(o.isValid())
|
||||
{
|
||||
return getDCSCode().compareTo(o.getDCSCode());
|
||||
}
|
||||
else
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.radio;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Fully qualified radio. Note: this is only for P25.
|
||||
*/
|
||||
public class P25FullyQualifiedRadio extends Radio
|
||||
{
|
||||
private int mWacn;
|
||||
private int mSystem;
|
||||
|
||||
public P25FullyQualifiedRadio()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param wacn for the radio
|
||||
* @param system for the radio
|
||||
* @param id for the radio
|
||||
*/
|
||||
public P25FullyQualifiedRadio(int wacn, int system, int id)
|
||||
{
|
||||
super(Protocol.APCO25, id);
|
||||
mWacn = wacn;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.P25_FULLY_QUALIFIED_RADIO_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* WACN for this radio
|
||||
* @return wacn
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||
public int getWacn()
|
||||
{
|
||||
return mWacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the WACN for this radio
|
||||
* @param wacn for this radio
|
||||
*/
|
||||
public void setWacn(int wacn)
|
||||
{
|
||||
mWacn = wacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* System for this radio
|
||||
* @return system
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||
public int getSystem()
|
||||
{
|
||||
return mSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system for this radio
|
||||
* @param system for this radio
|
||||
*/
|
||||
public void setSystem(int system)
|
||||
{
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
sb.append(" Protocol:").append((getProtocol()));
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashable string key for use in a lookup map
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHashKey()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.alias.id.talkgroup;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.alias.id.AliasIDType;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
|
||||
/**
|
||||
* Fully qualified talkgroup. Note: this is only for P25.
|
||||
*/
|
||||
public class P25FullyQualifiedTalkgroup extends Talkgroup
|
||||
{
|
||||
private int mWacn;
|
||||
private int mSystem;
|
||||
|
||||
public P25FullyQualifiedTalkgroup()
|
||||
{
|
||||
//No-arg JAXB constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param wacn for the radio
|
||||
* @param system for the radio
|
||||
* @param talkgroup value
|
||||
*/
|
||||
public P25FullyQualifiedTalkgroup(int wacn, int system, int talkgroup)
|
||||
{
|
||||
super(Protocol.APCO25, talkgroup);
|
||||
mWacn = wacn;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AliasIDType getType()
|
||||
{
|
||||
return AliasIDType.P25_FULLY_QUALIFIED_TALKGROUP;
|
||||
}
|
||||
|
||||
/**
|
||||
* WACN for this radio
|
||||
* @return wacn
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "wacn")
|
||||
public int getWacn()
|
||||
{
|
||||
return mWacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the WACN for this radio
|
||||
* @param wacn for this radio
|
||||
*/
|
||||
public void setWacn(int wacn)
|
||||
{
|
||||
mWacn = wacn;
|
||||
}
|
||||
|
||||
/**
|
||||
* System for this radio
|
||||
* @return system
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system")
|
||||
public int getSystem()
|
||||
{
|
||||
return mSystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system for this radio
|
||||
* @param system for this radio
|
||||
*/
|
||||
public void setSystem(int system)
|
||||
{
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Fully Qualified Radio ID:").append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
sb.append(" Protocol:").append((getProtocol()));
|
||||
|
||||
if(!isValid())
|
||||
{
|
||||
sb.append(" **NOT VALID**");
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashable string key for use in a lookup map
|
||||
* @return
|
||||
*/
|
||||
@JsonIgnore
|
||||
public String getHashKey()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(getWacn());
|
||||
sb.append(".").append(getSystem());
|
||||
sb.append(".").append(getValue());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -43,6 +43,7 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
|
||||
private static float[] sHighPassFilterCoefficients;
|
||||
private final boolean mAudioFilterEnable;
|
||||
|
||||
static
|
||||
{
|
||||
@@ -70,27 +71,33 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
}
|
||||
}
|
||||
|
||||
private IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
|
||||
private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
private SquelchState mSquelchState = SquelchState.SQUELCH;
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
*
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param timeslot for this audio module
|
||||
* @param maxAudioSegmentLength in milliseconds
|
||||
* @param audioFilterEnable to enable or disable high-pass audio filter
|
||||
*/
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength)
|
||||
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, boolean audioFilterEnable)
|
||||
{
|
||||
super(aliasList, timeslot, maxAudioSegmentLength);
|
||||
mAudioFilterEnable = audioFilterEnable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Audio Module.
|
||||
* @param aliasList for aliasing identifiers
|
||||
* @param audioFilterEnable to enable or disable high-pass audio filter
|
||||
*/
|
||||
public AudioModule(AliasList aliasList)
|
||||
public AudioModule(AliasList aliasList, boolean audioFilterEnable)
|
||||
{
|
||||
super(aliasList);
|
||||
mAudioFilterEnable = audioFilterEnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,7 +128,11 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
|
||||
{
|
||||
if(mSquelchState == SquelchState.UNSQUELCH)
|
||||
{
|
||||
audioBuffer = mHighPassFilter.filter(audioBuffer);
|
||||
if(mAudioFilterEnable)
|
||||
{
|
||||
audioBuffer = mHighPassFilter.filter(audioBuffer);
|
||||
}
|
||||
|
||||
addAudio(audioBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -27,6 +27,7 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.encryption.EncryptionKeyIdentifier;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Collection;
|
||||
@@ -68,6 +69,7 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioSegment.class);
|
||||
private BooleanProperty mComplete = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mDuplicate = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mEncrypted = new SimpleBooleanProperty(false);
|
||||
private BooleanProperty mRecordAudio = new SimpleBooleanProperty(false);
|
||||
private IntegerProperty mMonitorPriority = new SimpleIntegerProperty(Priority.DEFAULT_PRIORITY);
|
||||
private ObservableSet<BroadcastChannel> mBroadcastChannels = FXCollections.observableSet(new HashSet<>());
|
||||
@@ -129,6 +131,25 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
return (mSampleCount / 8); //8 kHz audio generates 8 samples per millisecond
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio segment contains encrypted audio.
|
||||
*
|
||||
* @return encrypted property
|
||||
*/
|
||||
public BooleanProperty encryptedProperty()
|
||||
{
|
||||
return mEncrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this audio segment is encrypted
|
||||
* @return true if encrypted.
|
||||
*/
|
||||
public boolean isEncrypted()
|
||||
{
|
||||
return mEncrypted.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete property is used by the audio segment producer to signal that the segment is complete and no
|
||||
* additional audio or identifiers will be added to the segment.
|
||||
@@ -386,6 +407,11 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
throw new IllegalStateException("Can't add audio to an audio segment that is being disposed");
|
||||
}
|
||||
|
||||
if(mAudioBuffers.isEmpty())
|
||||
{
|
||||
mStartTimestamp = System.currentTimeMillis() - 20;
|
||||
}
|
||||
|
||||
mAudioBuffers.add(audioBuffer);
|
||||
mSampleCount += audioBuffer.length;
|
||||
}
|
||||
@@ -415,6 +441,14 @@ public class AudioSegment implements Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
mIdentifierCollection.update(identifier);
|
||||
|
||||
/**
|
||||
* If we have a late-add encryption key, set the encrypted flag to true.
|
||||
*/
|
||||
if(identifier instanceof EncryptionKeyIdentifier eki)
|
||||
{
|
||||
mEncrypted.set(eki.isEncrypted());
|
||||
}
|
||||
|
||||
List<Alias> aliases = mAliasList.getAliases(identifier);
|
||||
|
||||
for(Alias alias: aliases)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -28,12 +28,9 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.duplicate.DuplicateCallDetectionPreference;
|
||||
import io.github.dsheirer.preference.duplicate.ICallManagementProvider;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -41,7 +38,10 @@ import java.util.Map;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Detects duplicate calls that occur within the same system. This detector is thread safe for the receive() method.
|
||||
@@ -52,18 +52,41 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DuplicateCallDetector.class);
|
||||
private DuplicateCallDetectionPreference mDuplicateCallDetectionPreference;
|
||||
private ICallManagementProvider mCallManagementProvider;
|
||||
private Map<String,SystemDuplicateCallDetector> mDetectorMap = new HashMap();
|
||||
protected Listener<AudioSegment> mDuplicateCallDetectionListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param userPreferences to access the duplicate call detection preferences.
|
||||
*/
|
||||
public DuplicateCallDetector(UserPreferences userPreferences)
|
||||
{
|
||||
mDuplicateCallDetectionPreference = userPreferences.getDuplicateCallDetectionPreference();
|
||||
this(userPreferences.getCallManagementPreference());
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
* @param callManagementProvider to provide call management preferences.
|
||||
*/
|
||||
public DuplicateCallDetector(ICallManagementProvider callManagementProvider)
|
||||
{
|
||||
mCallManagementProvider = callManagementProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional listener to be notified each time an audio segment is flagged as duplicate.
|
||||
* @param listener to register
|
||||
*/
|
||||
public void setDuplicateCallDetectionListener(Listener<AudioSegment> listener)
|
||||
{
|
||||
mDuplicateCallDetectionListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioSegment audioSegment)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionEnabled())
|
||||
{
|
||||
Identifier identifier = audioSegment.getIdentifierCollection()
|
||||
.getIdentifier(IdentifierClass.CONFIGURATION, Form.SYSTEM, Role.ANY);
|
||||
@@ -78,7 +101,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
|
||||
if(detector == null)
|
||||
{
|
||||
detector = new SystemDuplicateCallDetector();
|
||||
detector = new SystemDuplicateCallDetector(mCallManagementProvider, system);
|
||||
mDetectorMap.put(system, detector);
|
||||
}
|
||||
|
||||
@@ -88,45 +111,99 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* System level duplicate call detector. Uses a scheduled executor to run every 25 ms to compare all ongoing call
|
||||
* audio segments to detect duplicates.
|
||||
*
|
||||
* All audio segments remain in the queue until they are flagged as complete. While in the queue, each call is
|
||||
* compared against the others to detect duplicates. Once all calls are either flagged as complete or flagged as
|
||||
* duplicate and removed, the queue is empty and the monitoring is shutdown until a new audio segment arrives and
|
||||
* then the monitoring starts again.
|
||||
*/
|
||||
public class SystemDuplicateCallDetector
|
||||
{
|
||||
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private AtomicBoolean mMonitoring = new AtomicBoolean();
|
||||
private final LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private final List<AudioSegment> mAudioSegments = new ArrayList<>();
|
||||
private ScheduledFuture<?> mProcessorFuture;
|
||||
private Lock mLock = new ReentrantLock();
|
||||
private boolean mMonitoring = false;
|
||||
private final ICallManagementProvider mCallManagementProvider;
|
||||
private String mSystem;
|
||||
|
||||
public SystemDuplicateCallDetector()
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param callManagementProvider to check for duplicate monitoring preferences
|
||||
*/
|
||||
public SystemDuplicateCallDetector(ICallManagementProvider callManagementProvider, String system)
|
||||
{
|
||||
mCallManagementProvider = callManagementProvider;
|
||||
mSystem = system;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the audio segment to the monitoring queue.
|
||||
* @param audioSegment to add
|
||||
*/
|
||||
public void add(AudioSegment audioSegment)
|
||||
{
|
||||
//Block on audio segment queue so that we don't interfere with monitoring shutdown
|
||||
synchronized(mAudioSegmentQueue)
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mAudioSegmentQueue.add(audioSegment);
|
||||
startMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
private void startMonitoring()
|
||||
{
|
||||
if(mMonitoring.compareAndSet(false, true))
|
||||
finally
|
||||
{
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(() -> process(),
|
||||
0, 25, TimeUnit.MILLISECONDS);
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the call monitoring thread if it's not already running.
|
||||
*
|
||||
* Note: this method should only be called from a thread with the lock acquired.
|
||||
*/
|
||||
private void startMonitoring()
|
||||
{
|
||||
if(!mMonitoring)
|
||||
{
|
||||
mProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(this::process,
|
||||
0, 25, TimeUnit.MILLISECONDS);
|
||||
mMonitoring = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the call monitoring thread if the audio segements queue is empty.
|
||||
*
|
||||
* Note: this should only be called from a separate thread and not from within the scheduled monitoring thread
|
||||
* because cancelling the scheduled timer from within the process() method would kill the thread without
|
||||
* releasing the lock, causing a deadlock.
|
||||
*/
|
||||
private void stopMonitoring()
|
||||
{
|
||||
if(mMonitoring.compareAndSet(true, false))
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(mProcessorFuture != null)
|
||||
//Recheck the audio segments queue to make sure we didn't slip in another audio segment before we can
|
||||
//shut down the scheduled monitoring thread.
|
||||
if(mMonitoring && mAudioSegments.isEmpty())
|
||||
{
|
||||
mProcessorFuture.cancel(true);
|
||||
if(mProcessorFuture != null)
|
||||
{
|
||||
mProcessorFuture.cancel(true);
|
||||
mProcessorFuture = null;
|
||||
}
|
||||
|
||||
mMonitoring = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +214,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
*/
|
||||
private boolean isDuplicate(AudioSegment segment1, AudioSegment segment2)
|
||||
{
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionByTalkgroupEnabled())
|
||||
{
|
||||
//Step 1 check for duplicate TO values
|
||||
List<Identifier> to1 = segment1.getIdentifierCollection().getIdentifiers(Role.TO);
|
||||
@@ -149,7 +226,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
if(mDuplicateCallDetectionPreference.isDuplicateCallDetectionByRadioEnabled())
|
||||
if(mCallManagementProvider.isDuplicateCallDetectionByRadioEnabled())
|
||||
{
|
||||
//Step 2 check for duplicate FROM values
|
||||
List<Identifier> from1 = segment1.getIdentifierCollection().getIdentifiers(Role.FROM);
|
||||
@@ -169,53 +246,51 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
* @param identifiers2
|
||||
* @return
|
||||
*/
|
||||
private boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
|
||||
public static boolean isDuplicate(List<Identifier> identifiers1, List<Identifier> identifiers2)
|
||||
{
|
||||
for(Identifier identifier1: identifiers1)
|
||||
{
|
||||
if(identifier1 instanceof TalkgroupIdentifier)
|
||||
if(identifier1 instanceof TalkgroupIdentifier tgId1)
|
||||
{
|
||||
int talkgroup1 = ((TalkgroupIdentifier)identifier1).getValue();
|
||||
int tg1 = tgId1.getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof TalkgroupIdentifier &&
|
||||
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
|
||||
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == tg1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(identifier2 instanceof PatchGroupIdentifier &&
|
||||
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
|
||||
pgId2.getValue().getPatchGroup().getValue() == tg1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier1 instanceof PatchGroupIdentifier)
|
||||
else if(identifier1 instanceof PatchGroupIdentifier pgId1)
|
||||
{
|
||||
int talkgroup1 = ((PatchGroupIdentifier)identifier1).getValue().getPatchGroup().getValue();
|
||||
int talkgroup1 = pgId1.getValue().getPatchGroup().getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof TalkgroupIdentifier &&
|
||||
((TalkgroupIdentifier)identifier2).getValue() == talkgroup1)
|
||||
if(identifier2 instanceof TalkgroupIdentifier tgId2 && tgId2.getValue() == talkgroup1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if(identifier2 instanceof PatchGroupIdentifier &&
|
||||
((PatchGroupIdentifier)identifier2).getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
else if(identifier2 instanceof PatchGroupIdentifier pgId2 &&
|
||||
pgId2.getValue().getPatchGroup().getValue() == talkgroup1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(identifier1 instanceof RadioIdentifier)
|
||||
else if(identifier1 instanceof RadioIdentifier raId1)
|
||||
{
|
||||
int radio1 = ((RadioIdentifier)identifier1).getValue();
|
||||
int radio1 = raId1.getValue();
|
||||
|
||||
for(Identifier identifier2: identifiers2)
|
||||
{
|
||||
if(identifier2 instanceof RadioIdentifier && ((RadioIdentifier)identifier2).getValue() == radio1)
|
||||
if(identifier2 instanceof RadioIdentifier raId2 && raId2.getValue() == radio1)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -231,6 +306,8 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
*/
|
||||
private void process()
|
||||
{
|
||||
mLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
//Transfer in newly arrived audio segments
|
||||
@@ -248,6 +325,19 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
return complete;
|
||||
});
|
||||
|
||||
//Remove any encrypted audio segments.
|
||||
mAudioSegments.removeIf(audioSegment -> {
|
||||
boolean encrypted = audioSegment.isEncrypted();
|
||||
|
||||
if(encrypted)
|
||||
{
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
|
||||
|
||||
return encrypted;
|
||||
});
|
||||
|
||||
//Only check for duplicates if there is more than one call
|
||||
if(mAudioSegments.size() > 1)
|
||||
{
|
||||
@@ -258,7 +348,7 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
{
|
||||
AudioSegment current = mAudioSegments.get(currentIndex);
|
||||
|
||||
if(!current.isDuplicate())
|
||||
if(current.hasAudio() && !current.isDuplicate())
|
||||
{
|
||||
int checkIndex = currentIndex + 1;
|
||||
|
||||
@@ -273,6 +363,12 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
toCheck.setDuplicate(true);
|
||||
toCheck.decrementConsumerCount();
|
||||
duplicates.add(toCheck);
|
||||
|
||||
//Notify optional listener that we flagged the call as duplicate.
|
||||
if(mDuplicateCallDetectionListener != null)
|
||||
{
|
||||
mDuplicateCallDetectionListener.receive(toCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,26 +382,12 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
mAudioSegments.removeAll(duplicates);
|
||||
}
|
||||
|
||||
//Finally, if the audio segment queue is empty, shutdown montitoring until a new segment arrives
|
||||
//Finally, if the audio segment queue is now empty, shutdown monitoring until a new segment arrives.
|
||||
//The monitor shutdown method has to be called on a separate thread so that we don't kill our current
|
||||
// thread and fail to release the lock.
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
//Block on the audio segment queue so that we can shutdown before any new segments are added, and
|
||||
//allow the add(segment) to restart monitoring as soon as needed.
|
||||
synchronized(mAudioSegmentQueue)
|
||||
{
|
||||
if(mAudioSegmentQueue.isEmpty())
|
||||
{
|
||||
try
|
||||
{
|
||||
stopMonitoring();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unexpected error during duplicate audio segment monitoring shutdown", e);
|
||||
//Do nothing, we got interrupted
|
||||
}
|
||||
}
|
||||
}
|
||||
ThreadPool.CACHED.submit(this::stopMonitoring);
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
@@ -313,6 +395,10 @@ public class DuplicateCallDetector implements Listener<AudioSegment>
|
||||
mLog.error("Unknown error while processing audio segments for duplicate call detection. Please report " +
|
||||
"this to the developer.", t);
|
||||
}
|
||||
finally
|
||||
{
|
||||
mLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
private Listener<BroadcastEvent> mBroadcastEventListener;
|
||||
private T mBroadcastConfiguration;
|
||||
protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY);
|
||||
protected ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
|
||||
protected int mStreamedAudioCount = 0;
|
||||
protected int mErrorAudioCount = 0;
|
||||
protected int mAgedOffAudioCount = 0;
|
||||
@@ -55,6 +56,14 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable last bad broadcast state property
|
||||
*/
|
||||
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
|
||||
{
|
||||
return mLastBadBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current state of the broadcastAudio connection
|
||||
*/
|
||||
@@ -68,10 +77,26 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
|
||||
*/
|
||||
public void setBroadcastState(BroadcastState broadcastState)
|
||||
{
|
||||
if(broadcastState == BroadcastState.CONNECTED)
|
||||
{
|
||||
mLastBadBroadcastState.setValue(null);
|
||||
}
|
||||
else if(broadcastState.isErrorState() || broadcastState.isWarningState())
|
||||
{
|
||||
mLastBadBroadcastState.setValue(broadcastState);
|
||||
}
|
||||
mBroadcastState.setValue(broadcastState);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Last bad state of the broadcastAudio connection
|
||||
*/
|
||||
public BroadcastState getLastBadBroadcastState()
|
||||
{
|
||||
return mLastBadBroadcastState.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the broadcaster
|
||||
*/
|
||||
|
||||
@@ -211,9 +211,17 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
|
||||
super.setBroadcastState(state);
|
||||
|
||||
if(mBroadcastState.get() != null && mBroadcastState.get().isErrorState())
|
||||
if(mBroadcastState.get() != null)
|
||||
{
|
||||
stop();
|
||||
if(mBroadcastState.get().isErrorState())
|
||||
{
|
||||
mLog.error("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
|
||||
stop();
|
||||
}
|
||||
else if(mBroadcastState.get().isWarningState())
|
||||
{
|
||||
mLog.warn("[" + getStreamName() + "] status: " + mBroadcastState.get().toString());
|
||||
}
|
||||
}
|
||||
|
||||
if(!connected())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,25 +19,34 @@
|
||||
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
|
||||
import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.record.AudioSegmentRecorder;
|
||||
import io.github.dsheirer.record.RecordFormat;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Audio streaming manager monitors audio segments through completion and creates temporary streaming recordings on
|
||||
@@ -127,7 +136,7 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
{
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDuplicate() && mUserPreferences.getDuplicateCallDetectionPreference().isDuplicateStreamingSuppressionEnabled())
|
||||
if(audioSegment.isDuplicate() && mUserPreferences.getCallManagementPreference().isDuplicateStreamingSuppressionEnabled())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
@@ -138,29 +147,60 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
|
||||
if(mAudioRecordingListener != null && audioSegment.hasBroadcastChannels())
|
||||
{
|
||||
Path path = getTemporaryRecordingPath();
|
||||
long length = 0;
|
||||
|
||||
for(float[] audioBuffer: audioSegment.getAudioBuffers())
|
||||
{
|
||||
length += audioBuffer.length;
|
||||
}
|
||||
|
||||
length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences);
|
||||
IdentifierCollection identifierCollectionCopy =
|
||||
IdentifierCollection identifiers =
|
||||
new IdentifierCollection(audioSegment.getIdentifierCollection().getIdentifiers());
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, audioSegment.getBroadcastChannels(),
|
||||
identifierCollectionCopy, audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
if(identifiers.getToIdentifier() instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
mLog.error("Error recording temporary stream MP3");
|
||||
if(mUserPreferences.getCallManagementPreference()
|
||||
.getPatchGroupStreamingOption() == PatchGroupStreamingOption.TALKGROUPS)
|
||||
{
|
||||
//Decompose the patch group into the individual (patched) talkgroups and process the audio
|
||||
//segment for each patched talkgroup.
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
List<Identifier> ids = new ArrayList<>();
|
||||
ids.addAll(patchGroup.getPatchedTalkgroupIdentifiers());
|
||||
ids.addAll(patchGroup.getPatchedRadioIdentifiers());
|
||||
|
||||
//If there are no patched radios/talkgroups, override user preference and stream as a patch group
|
||||
if(ids.isEmpty() || audioSegment.getAliasList() == null)
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
else
|
||||
{
|
||||
AliasList aliasList = audioSegment.getAliasList();
|
||||
|
||||
for(Identifier identifier: ids)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
Set<BroadcastChannel> broadcastChannels = new HashSet<>();
|
||||
for(Alias alias: aliases)
|
||||
{
|
||||
broadcastChannels.addAll(alias.getBroadcastChannels());
|
||||
}
|
||||
|
||||
if(!broadcastChannels.isEmpty())
|
||||
{
|
||||
MutableIdentifierCollection decomposedIdentifiers =
|
||||
new MutableIdentifierCollection(identifiers.getIdentifiers());
|
||||
//Remove patch group TO identifier & replace with the patched talkgroup/radio
|
||||
decomposedIdentifiers.remove(Role.TO);
|
||||
decomposedIdentifiers.update(identifier);
|
||||
processAudioSegment(audioSegment, decomposedIdentifiers, broadcastChannels);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
processAudioSegment(audioSegment, identifiers, audioSegment.getBroadcastChannels());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +209,40 @@ public class AudioStreamingManager implements Listener<AudioSegment>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes an audio segment for streaming by creating a temporary MP3 recording and submitting the recording
|
||||
* to the specific broadcast channel(s).
|
||||
* @param audioSegment to process for streaming
|
||||
* @param identifierCollection to use for the streamed audio recording.
|
||||
* @param broadcastChannels to receive the audio recording
|
||||
*/
|
||||
private void processAudioSegment(AudioSegment audioSegment, IdentifierCollection identifierCollection,
|
||||
Set<BroadcastChannel> broadcastChannels)
|
||||
{
|
||||
Path path = getTemporaryRecordingPath();
|
||||
long length = 0;
|
||||
|
||||
for(float[] audioBuffer: audioSegment.getAudioBuffers())
|
||||
{
|
||||
length += audioBuffer.length;
|
||||
}
|
||||
|
||||
length /= 8; //Sample rate is 8000 samples per second, or 8 samples per millisecond.
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.record(audioSegment, path, RecordFormat.MP3, mUserPreferences, identifierCollection);
|
||||
|
||||
AudioRecording audioRecording = new AudioRecording(path, broadcastChannels, identifierCollection,
|
||||
audioSegment.getStartTimestamp(), length);
|
||||
mAudioRecordingListener.receive(audioRecording);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error recording temporary stream MP3");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a temporary streaming recording file path
|
||||
*/
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyCallConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v1.ShoutcastV1Configuration;
|
||||
import io.github.dsheirer.audio.broadcast.shoutcast.v2.ShoutcastV2Configuration;
|
||||
@@ -48,6 +49,7 @@ import java.net.SocketAddress;
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = BroadcastifyCallConfiguration.class, name="broadcastifyCallConfiguration"),
|
||||
@JsonSubTypes.Type(value = RdioScannerConfiguration.class, name="RdioScannerConfiguration"),
|
||||
@JsonSubTypes.Type(value = OpenMHzConfiguration.class, name="OpenMHzConfiguration"),
|
||||
@JsonSubTypes.Type(value = IcecastConfiguration.class, name="icecastConfiguration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV1Configuration.class, name="shoutcastV1Configuration"),
|
||||
@JsonSubTypes.Type(value = ShoutcastV2Configuration.class, name="shoutcastV2Configuration"),
|
||||
|
||||
@@ -25,6 +25,9 @@ import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfigura
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.rdioscanner.RdioScannerFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastHTTPConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPAudioBroadcaster;
|
||||
@@ -67,6 +70,9 @@ public class BroadcastFactory
|
||||
case RDIOSCANNER_CALL:
|
||||
return new RdioScannerBroadcaster((RdioScannerConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzBroadcaster((OpenMHzConfiguration)configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
case BROADCASTIFY:
|
||||
return new IcecastTCPAudioBroadcaster((BroadcastifyFeedConfiguration) configuration,
|
||||
inputAudioFormat, mp3Setting, aliasModel);
|
||||
@@ -109,6 +115,8 @@ public class BroadcastFactory
|
||||
return new RdioScannerConfiguration(format);
|
||||
case BROADCASTIFY:
|
||||
return new BroadcastifyFeedConfiguration(format);
|
||||
case OPENMHZ:
|
||||
return new OpenMHzConfiguration(format);
|
||||
case ICECAST_HTTP:
|
||||
return new IcecastHTTPConfiguration(format);
|
||||
case ICECAST_TCP:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -344,14 +344,13 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
deleteBroadcaster(configuredBroadcast);
|
||||
}
|
||||
|
||||
AbstractAudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration,
|
||||
final AbstractAudioBroadcaster audioBroadcaster = BroadcastFactory.getBroadcaster(broadcastConfiguration,
|
||||
mAliasModel, mUserPreferences);
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
configuredBroadcast.setAudioBroadcaster(audioBroadcaster);
|
||||
audioBroadcaster.setListener(mBroadcastEventListener);
|
||||
audioBroadcaster.start();
|
||||
mBroadcasterMap.put(audioBroadcaster.getBroadcastConfiguration().getId(), audioBroadcaster);
|
||||
|
||||
int index = mConfiguredBroadcasts.indexOf(configuredBroadcast);
|
||||
@@ -362,6 +361,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
}
|
||||
|
||||
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
|
||||
ThreadPool.CACHED.submit(audioBroadcaster::start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public enum BroadcastServerType
|
||||
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||
RDIOSCANNER_CALL("Rdio Scanner", "images/rdioscanner.png"),
|
||||
OPENMHZ("OpenMHz", "images/openmhz.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||
|
||||
@@ -23,90 +23,92 @@ public enum BroadcastState
|
||||
/**
|
||||
* General error in configuration that causes the remote server to reject the connection
|
||||
*/
|
||||
CONFIGURATION_ERROR("Configuration Error", true),
|
||||
CONFIGURATION_ERROR("Configuration Error", true, false),
|
||||
|
||||
/**
|
||||
* Connected to the broadcastAudio server and capable of streaming
|
||||
*/
|
||||
CONNECTED("Connected", false),
|
||||
CONNECTED("Connected", false, false),
|
||||
|
||||
/**
|
||||
* Connection interrupted, attempting to reconnect.
|
||||
*/
|
||||
CONNECTING("Connecting", false),
|
||||
CONNECTING("Connecting", false, false),
|
||||
|
||||
/**
|
||||
* Indicates the configuration is disabled
|
||||
*/
|
||||
DISABLED("Disabled", true),
|
||||
DISABLED("Disabled", true, false),
|
||||
|
||||
/**
|
||||
* Disconnected from the broadcastAudio server
|
||||
*/
|
||||
DISCONNECTED("Disconnected", false),
|
||||
DISCONNECTED("Disconnected", false, false),
|
||||
|
||||
/**
|
||||
* Invalid credentials - user name or password
|
||||
*/
|
||||
INVALID_CREDENTIALS("Invalid User Name/Password", true),
|
||||
INVALID_CREDENTIALS("Invalid User Name/Password", true, false),
|
||||
|
||||
/**
|
||||
* Invalid mount point
|
||||
*/
|
||||
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true),
|
||||
INVALID_MOUNT_POINT("Invalid Mount/Stream ID", true, false),
|
||||
|
||||
/**
|
||||
* Invalid configuration settings
|
||||
*/
|
||||
INVALID_SETTINGS("Invalid Settings", true),
|
||||
INVALID_SETTINGS("Invalid Settings", true, false),
|
||||
|
||||
/**
|
||||
* Remote server max sources has been exceeded
|
||||
*/
|
||||
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", true),
|
||||
MAX_SOURCES_EXCEEDED("Max Sources Exceeded", false, true),
|
||||
|
||||
/**
|
||||
* Specified mount point is already in use
|
||||
*/
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false, true),
|
||||
|
||||
/**
|
||||
* Network is unavailable or the server address cannot be resolved
|
||||
*/
|
||||
NETWORK_UNAVAILABLE("Network Unavailable", false),
|
||||
NETWORK_UNAVAILABLE("Network Unavailable", false, false),
|
||||
|
||||
/**
|
||||
* Server is not known or reachable
|
||||
*/
|
||||
NO_SERVER("No Server", false),
|
||||
NO_SERVER("No Server", false, false),
|
||||
|
||||
/**
|
||||
* Initial state with no connection attempted.
|
||||
*/
|
||||
READY("Ready", false),
|
||||
READY("Ready", false, false),
|
||||
|
||||
/**
|
||||
* Error while broadcasting stream data. Temporary error state to allow connection to be reset.
|
||||
*/
|
||||
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false),
|
||||
TEMPORARY_BROADCAST_ERROR("Temporary Broadcast Error", false, false),
|
||||
|
||||
/**
|
||||
* Unsupported audio format
|
||||
*/
|
||||
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true),
|
||||
UNSUPPORTED_AUDIO_FORMAT("Unsupported Audio Type", true, false),
|
||||
|
||||
/**
|
||||
* Unspecified error
|
||||
*/
|
||||
ERROR("Error", true);
|
||||
ERROR("Error", true, false);
|
||||
|
||||
private String mLabel;
|
||||
private boolean mErrorState;
|
||||
private boolean mWarningState;
|
||||
|
||||
BroadcastState(String label, boolean errorState)
|
||||
BroadcastState(String label, boolean errorState, boolean warningState)
|
||||
{
|
||||
mLabel = label;
|
||||
mErrorState = errorState;
|
||||
mWarningState = warningState;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
@@ -118,4 +120,9 @@ public enum BroadcastState
|
||||
{
|
||||
return mErrorState;
|
||||
}
|
||||
|
||||
public boolean isWarningState()
|
||||
{
|
||||
return mWarningState;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ConfiguredBroadcast
|
||||
private BroadcastConfiguration mBroadcastConfiguration;
|
||||
private AbstractAudioBroadcaster mAudioBroadcaster;
|
||||
private ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>();
|
||||
private ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -89,6 +90,14 @@ public class ConfiguredBroadcast
|
||||
return mBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last bad broadcast state of the configured audio broadcaster (optional)
|
||||
*/
|
||||
public ObjectProperty<BroadcastState> lastBadBroadcastStateProperty()
|
||||
{
|
||||
return mLastBadBroadcastState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the audio broadcaster
|
||||
* @param audioBroadcaster to use for this configuration
|
||||
@@ -96,11 +105,13 @@ public class ConfiguredBroadcast
|
||||
public void setAudioBroadcaster(AbstractAudioBroadcaster audioBroadcaster)
|
||||
{
|
||||
mBroadcastState.unbind();
|
||||
mLastBadBroadcastState.unbind();
|
||||
mAudioBroadcaster = audioBroadcaster;
|
||||
|
||||
if(audioBroadcaster != null)
|
||||
{
|
||||
mBroadcastState.bind(mAudioBroadcaster.broadcastStateProperty());
|
||||
mLastBadBroadcastState.bind(mAudioBroadcaster.lastBadBroadcastStateProperty());
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -120,6 +131,7 @@ public class ConfiguredBroadcast
|
||||
{
|
||||
mBroadcastState.setValue(BroadcastState.CONFIGURATION_ERROR);
|
||||
}
|
||||
mLastBadBroadcastState.setValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
/**
|
||||
* Options for streaming management of patch groups. These options are used when streaming an audio call that is
|
||||
* tagged to a patch group. The audio will either be streamed once and identified as the patch group, or it will
|
||||
* be streamed multiple times, once for each individual patched talkgroup.
|
||||
*/
|
||||
public enum PatchGroupStreamingOption
|
||||
{
|
||||
PATCH_GROUP("Patch Group"),
|
||||
TALKGROUPS("Individual Talkgroups");
|
||||
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param label to display
|
||||
*/
|
||||
PatchGroupStreamingOption(String label)
|
||||
{
|
||||
mLabel = label;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mLabel;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -38,9 +38,6 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -48,12 +45,13 @@ import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the Broadcastify call push API.
|
||||
@@ -475,7 +473,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.broadcastify;
|
||||
|
||||
@@ -35,6 +32,7 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
|
||||
private final static Logger mLog = LoggerFactory.getLogger(BroadcastifyFeedConfiguration.class);
|
||||
|
||||
private int mFeedID;
|
||||
private boolean mVerboseLogging = false;
|
||||
|
||||
public BroadcastifyFeedConfiguration()
|
||||
{
|
||||
@@ -123,4 +121,15 @@ public class BroadcastifyFeedConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "verbose_logging")
|
||||
public boolean isVerboseLogging()
|
||||
{
|
||||
return mVerboseLogging;
|
||||
}
|
||||
|
||||
public void setVerboseLogging(boolean verboseLogging)
|
||||
{
|
||||
mVerboseLogging = verboseLogging;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,15 +341,29 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP 403 protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
case 401: //Unauthorized
|
||||
if(message.toString().contains("Authentication Required"))
|
||||
{
|
||||
mLog.error("Stream [" + getStreamName() + "] - unable to connect - invalid credentials");
|
||||
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP 401 protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -401,9 +415,16 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
disconnect();
|
||||
break;
|
||||
default:
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
disconnect();
|
||||
/**
|
||||
* Only allow a generic error to update state if we've not already experienced a more
|
||||
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
|
||||
*/
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
mLog.debug("Unspecified error: " + response.toString() + " Class:" + object.getClass());
|
||||
disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.broadcast.icecast;
|
||||
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.broadcast.broadcastify.BroadcastifyFeedConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.codec.IcecastCodecFactory;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||
@@ -39,6 +40,8 @@ import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
import org.apache.mina.core.session.IoSession;
|
||||
import org.apache.mina.filter.codec.ProtocolCodecFilter;
|
||||
import org.apache.mina.filter.logging.LogLevel;
|
||||
import org.apache.mina.filter.logging.LoggingFilter;
|
||||
import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -56,6 +59,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
private IoSession mStreamingSession = null;
|
||||
|
||||
private long mLastConnectionAttempt = 0;
|
||||
private boolean mVerboseLogging = false;
|
||||
private AtomicBoolean mConnecting = new AtomicBoolean();
|
||||
|
||||
/**
|
||||
@@ -73,6 +77,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
{
|
||||
super(configuration, inputAudioFormat, mp3Setting, aliasModel);
|
||||
|
||||
if(configuration instanceof BroadcastifyFeedConfiguration broadcastify)
|
||||
{
|
||||
mVerboseLogging = broadcastify.isVerboseLogging();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +94,6 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
if(mInlineActive)
|
||||
{
|
||||
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
|
||||
if (mInlineRemaining == -1)
|
||||
{
|
||||
mInlineRemaining = mInlineInterval;
|
||||
@@ -96,16 +104,34 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
byte[] chunk = Arrays.copyOfRange(audio, audioOffset, Math.min(audioOffset + mInlineRemaining, audio.length));
|
||||
mInlineRemaining -= chunk.length;
|
||||
audioOffset += chunk.length;
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Broadcasting audio data - sending [" + chunk.length + "/" + audio.length + "] bytes");
|
||||
}
|
||||
|
||||
mStreamingSession.write(chunk);
|
||||
if(mInlineRemaining == 0)
|
||||
{
|
||||
mInlineRemaining = mInlineInterval;
|
||||
|
||||
byte[] metadata = IcecastMetadata.formatInline(IcecastMetadata.getTitle(identifierCollection, mAliasModel)).getBytes();
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Sending inline metadata: " + new String(metadata));
|
||||
}
|
||||
|
||||
mStreamingSession.write(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Broadcasting audio data [" + audio.length + "] bytes");
|
||||
}
|
||||
mStreamingSession.write(audio);
|
||||
}
|
||||
}
|
||||
@@ -132,9 +158,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
|
||||
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
|
||||
loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
|
||||
mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||
}
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec",
|
||||
new ProtocolCodecFilter(new IcecastCodecFactory()));
|
||||
@@ -148,6 +177,10 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Attempting connection ...");
|
||||
}
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
try
|
||||
@@ -156,19 +189,42 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
|
||||
getBroadcastConfiguration().getPort()));
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Socket created - asynchronous connect requested - entering wait period");
|
||||
}
|
||||
|
||||
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
|
||||
|
||||
if(connected)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Connected.");
|
||||
}
|
||||
|
||||
mStreamingSession = future.getSession();
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Not Connected. Connection attempt timeout [" + CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS + "ms] exceeded");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(RuntimeIoException rioe)
|
||||
{
|
||||
if(rioe.getCause() instanceof SocketException)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Socket error. This usually indicates sdrtrunk can't reach the server " +
|
||||
"address over the current network connection. Setting state to " +
|
||||
"NETWORK UNAVAILABLE", rioe);
|
||||
}
|
||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
@@ -176,12 +232,22 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
catch(UnresolvedAddressException uae)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Unresolved Address error. This means the domain name services can't resolve " +
|
||||
"the server URL to an IP address. Setting state to NETWORK UNAVAILABLE", uae);
|
||||
}
|
||||
|
||||
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
|
||||
mConnecting.set(false);
|
||||
return;
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Unknown error. An error occurred while attempting to connect to the server.", e);
|
||||
}
|
||||
mLog.error("Error", e);
|
||||
//Disregard ... we'll disconnect and try again
|
||||
}
|
||||
@@ -190,6 +256,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mLog.error("Throwable error caught", t);
|
||||
}
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Starting disconnect sequence since an error occurred while trying to connect.");
|
||||
}
|
||||
|
||||
disconnect();
|
||||
mConnecting.set(false);
|
||||
}
|
||||
@@ -209,6 +280,11 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
if(connected() && mStreamingSession != null)
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Routine disconnect requested from a connected state with a non-null streaming session");
|
||||
}
|
||||
|
||||
mStreamingSession.closeNow();
|
||||
}
|
||||
else
|
||||
@@ -217,6 +293,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
//want to preserve the error state that got us here, so the user can see it.
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Disconnect requested - previous non-error state was [" + getBroadcastState() +
|
||||
"] - changing state to DISCONNECTED");
|
||||
}
|
||||
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
@@ -282,12 +364,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mInlineActive = false;
|
||||
}
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Session opened. Sending: " + sb);
|
||||
}
|
||||
|
||||
|
||||
session.write(sb.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionClosed(IoSession session) throws Exception
|
||||
{
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Session closed. Setting connecting flag to false.");
|
||||
}
|
||||
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
//If there is already an error state, don't override it. Otherwise, set state to disconnected
|
||||
@@ -311,15 +404,23 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
|
||||
}
|
||||
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Session error caught.", cause);
|
||||
}
|
||||
|
||||
disconnect();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void messageReceived(IoSession session, Object object) throws Exception
|
||||
{
|
||||
if(object instanceof String)
|
||||
if(object instanceof String message)
|
||||
{
|
||||
String message = (String) object;
|
||||
if(mVerboseLogging)
|
||||
{
|
||||
mLog.info("Message Received [" + message + "]");
|
||||
}
|
||||
|
||||
if(message != null && !message.trim().isEmpty())
|
||||
{
|
||||
@@ -347,7 +448,15 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
else
|
||||
{
|
||||
mLog.error("Unrecognized server response:" + message);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
|
||||
/**
|
||||
* Only allow a generic error to update state if we've not already experienced a more
|
||||
* specific error. Otherwise, trailing messages will clear the more meaningful error state.
|
||||
*/
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
/**
|
||||
* HTTP headers used for posting to Rdio Scanner API
|
||||
*/
|
||||
public enum FormField
|
||||
{
|
||||
CALL("call"),
|
||||
FREQ("freq"),
|
||||
START_TIME("start_time"),
|
||||
STOP_TIME("stop_time"),
|
||||
CALL_LENGTH("call_length"),
|
||||
TALKGROUP_NUM("talkgroup_num"),
|
||||
EMERGENCY("emergency"),
|
||||
API_KEY("api_key"),
|
||||
SOURCE_LIST("source_list"),
|
||||
FREQ_LIST("freq_list"),
|
||||
PATCHES("patches");
|
||||
|
||||
private String mHeader;
|
||||
|
||||
FormField(String header)
|
||||
{
|
||||
mHeader = header;
|
||||
}
|
||||
|
||||
public String getHeader()
|
||||
{
|
||||
return mHeader;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,650 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.AudioRecording;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastState;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Audio broadcaster to push completed audio recordings to the OpenMHz call upload API.
|
||||
*
|
||||
*/
|
||||
public class OpenMHzBroadcaster extends AbstractAudioBroadcaster<OpenMHzConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzBroadcaster.class);
|
||||
|
||||
private static final String ENCODING_TYPE_MP3 = "mp3";
|
||||
private static final String MULTIPART_TYPE = "multipart";
|
||||
private static final String DEFAULT_SUBTYPE = "form-data";
|
||||
private static final String MULTIPART_FORM_DATA = MULTIPART_TYPE + "/" + DEFAULT_SUBTYPE;
|
||||
private static final String APPLICATION_FORM_URLENCODED = "application/x-www-form-urlencoded";
|
||||
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledFuture<?> mAudioRecordingProcessorFuture;
|
||||
private HttpClient mHttpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_2)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
private long mLastConnectionAttempt;
|
||||
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
|
||||
private AliasModel mAliasModel;
|
||||
|
||||
/**
|
||||
* Constructs an instance of the broadcaster
|
||||
* @param config to use
|
||||
* @param aliasModel for access to aliases
|
||||
*/
|
||||
public OpenMHzBroadcaster(OpenMHzConfiguration config, InputAudioFormat inputAudioFormat,
|
||||
MP3Setting mp3Setting, AliasModel aliasModel)
|
||||
{
|
||||
super(config);
|
||||
mAliasModel = aliasModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(response == "OK")
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if (response == "No Response")
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
mLog.error("Error connecting to OpenMHz server [Server not found or not reachable]");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("Error connecting to OpenMHz server on startup [" + response + "]");
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
}
|
||||
|
||||
if(mAudioRecordingProcessorFuture == null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new AudioRecordingProcessor(),
|
||||
0, 500, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the audio recording processor thread
|
||||
*/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
if(mAudioRecordingProcessorFuture != null)
|
||||
{
|
||||
mAudioRecordingProcessorFuture.cancel(true);
|
||||
mAudioRecordingProcessorFuture = null;
|
||||
dispose();
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares for disposal
|
||||
*/
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
audioRecording.removePendingReplay();
|
||||
audioRecording = mAudioRecordingQueue.poll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this broadcaster continues to have successful connections to and transactions with the remote
|
||||
* server. If there is a connectivity or other issue, the broadcast state is set to temporary error and
|
||||
* the audio processor thread will persistently invoke this method to attempt a reconnect.
|
||||
*/
|
||||
private boolean connected()
|
||||
{
|
||||
if(getBroadcastState() != BroadcastState.CONNECTED &&
|
||||
(System.currentTimeMillis() - mLastConnectionAttempt > mConnectionAttemptInterval))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTING);
|
||||
|
||||
String response = testConnection(getBroadcastConfiguration());
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
|
||||
if(response == "OK")
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else if (response == "No Response")
|
||||
{
|
||||
setBroadcastState(BroadcastState.NO_SERVER);
|
||||
mLog.error("Error reconnecting to OpenMHz server [Server not found or not reachable]");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
mLog.error("Error reconnecting to OpenMHz server [" + response + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return getBroadcastState() == BroadcastState.CONNECTED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAudioQueueSize()
|
||||
{
|
||||
return mAudioRecordingQueue.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(AudioRecording audioRecording)
|
||||
{
|
||||
mAudioRecordingQueue.offer(audioRecording);
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the audio recording is non-null and not too old, meaning that the age of the recording has not
|
||||
* exceeded the max age value indicated in the broadcast configuration. Audio recordings that are too old will be
|
||||
* deleted to ensure that the in-memory queue size doesn't blow up.
|
||||
* @param audioRecording to test
|
||||
* @return true if the recording is valid
|
||||
*/
|
||||
private boolean isValid(AudioRecording audioRecording)
|
||||
{
|
||||
return audioRecording != null && System.currentTimeMillis() - audioRecording.getStartTime() <=
|
||||
getBroadcastConfiguration().getMaximumRecordingAge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes any enqueued audio recordings. This method employs asynchronous
|
||||
* interaction with the server, so multiple audio recording uploads can occur simultaneously.
|
||||
*/
|
||||
private void processRecordingQueue()
|
||||
{
|
||||
|
||||
|
||||
while(connected() && !mAudioRecordingQueue.isEmpty())
|
||||
{
|
||||
final AudioRecording audioRecording = mAudioRecordingQueue.poll();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_QUEUE_CHANGE));
|
||||
|
||||
if(isValid(audioRecording) && audioRecording.getRecordingLength() > 0)
|
||||
{
|
||||
int durationSeconds = (int)(audioRecording.getRecordingLength() / 1E3f);
|
||||
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
|
||||
String talkgroup = getTo(audioRecording);
|
||||
String radioId = getFrom(audioRecording);
|
||||
Long frequency = getFrequency(audioRecording);
|
||||
String patches = getPatches(audioRecording);
|
||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
||||
String systemLabel = getSystemLabel(audioRecording);
|
||||
|
||||
try
|
||||
{
|
||||
byte[] audioBytes = null;
|
||||
|
||||
try
|
||||
{
|
||||
audioBytes = Files.readAllBytes(audioRecording.getPath());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
mLog.error("OpenMHz - audio recording file not found - ignoring upload");
|
||||
}
|
||||
|
||||
if(audioBytes != null)
|
||||
{
|
||||
String uri = getBroadcastConfiguration().getHost() + "/" + getBroadcastConfiguration().getSystemName() + "/upload";
|
||||
|
||||
OpenMHzBuilder bodyBuilder = new OpenMHzBuilder();
|
||||
bodyBuilder
|
||||
.addFile(audioBytes)
|
||||
.addPart(FormField.FREQ, frequency)
|
||||
.addPart(FormField.START_TIME, timestampSeconds)
|
||||
.addPart(FormField.STOP_TIME, timestampSeconds)
|
||||
.addPart(FormField.CALL_LENGTH, durationSeconds)
|
||||
.addPart(FormField.TALKGROUP_NUM, talkgroup)
|
||||
.addPart(FormField.EMERGENCY, 0)
|
||||
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
|
||||
// For future use if OpenMHz supports patch information
|
||||
//.addPart(FormField.PATCHES, patches)
|
||||
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
|
||||
|
||||
HttpRequest fileRequest = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
mHttpClient.sendAsync(fileRequest, HttpResponse.BodyHandlers.ofString())
|
||||
.whenComplete((fileResponse, throwable1) -> {
|
||||
if(throwable1 != null || fileResponse.statusCode() != 200)
|
||||
{
|
||||
if(throwable1 instanceof IOException || throwable1 instanceof CompletionException)
|
||||
{
|
||||
//We get socket reset exceptions occasionally when the remote server doesn't
|
||||
//fully read our request and immediately responds.
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
if(fileResponse.statusCode() == 200)
|
||||
{
|
||||
incrementStreamedAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_STREAMED_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.TEMPORARY_BROADCAST_ERROR);
|
||||
mLog.error("OpenMHz API file upload fail [" +
|
||||
fileResponse.statusCode() + "] response [" +
|
||||
fileResponse.body() + "]");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
//Register an error for the file not found exception
|
||||
mLog.error("OpenMHz API - upload file not found [" +
|
||||
audioRecording.getPath().toString() + "]");
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(OpenMHzBroadcaster.this,
|
||||
BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unknown Error", e);
|
||||
setBroadcastState(BroadcastState.ERROR);
|
||||
incrementErrorAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_ERROR_COUNT_CHANGE));
|
||||
audioRecording.removePendingReplay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we're not connected and there are recordings in the queue, check the recording at the head of the queue
|
||||
// and start age-off once the recordings become too old. The recordings should be time ordered in the queue.
|
||||
AudioRecording audioRecording = mAudioRecordingQueue.peek();
|
||||
|
||||
while(audioRecording != null)
|
||||
{
|
||||
if(isValid(audioRecording))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Remove the recording from the queue, remove a replay, and peek at the next recording in the queue
|
||||
mAudioRecordingQueue.poll();
|
||||
audioRecording.removePendingReplay();
|
||||
incrementAgedOffAudioCount();
|
||||
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_AGED_OFF_COUNT_CHANGE));
|
||||
audioRecording = mAudioRecordingQueue.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a frequency value from the audio recording identifier collection.
|
||||
*/
|
||||
private static Long getFrequency(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getIdentifier(IdentifierClass.CONFIGURATION,
|
||||
Form.CHANNEL_FREQUENCY, Role.ANY);
|
||||
|
||||
if(identifier instanceof ConfigurationLongIdentifier)
|
||||
{
|
||||
Long value = ((ConfigurationLongIdentifier)identifier).getValue();
|
||||
|
||||
if(value != null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return Long.valueOf(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the FROM identifier or uses a default of zero(0)
|
||||
*/
|
||||
private static String getFrom(AudioRecording audioRecording)
|
||||
{
|
||||
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
|
||||
{
|
||||
if(identifier instanceof RadioIdentifier)
|
||||
{
|
||||
return ((RadioIdentifier)identifier).getValue().toString();
|
||||
}
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
|
||||
*
|
||||
*/
|
||||
private static String getTo(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
|
||||
}
|
||||
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
|
||||
{
|
||||
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
|
||||
talkgroupIdentifier.getProtocol()));
|
||||
}
|
||||
else if(identifier instanceof RadioIdentifier radioIdentifier)
|
||||
{
|
||||
return radioIdentifier.getValue().toString();
|
||||
}
|
||||
|
||||
return "0";
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the Talkgroup Label from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first label as the primary talkgroup label.
|
||||
*
|
||||
*/
|
||||
private String getTalkgroupLabel(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(identifier != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(aliases.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the Talkgroup Group from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first group as the primary talkgroup group.
|
||||
*
|
||||
*/
|
||||
private String getTalkgroupGroup(AudioRecording audioRecording)
|
||||
{
|
||||
|
||||
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(identifier != null)
|
||||
{
|
||||
List<Alias> aliases = aliasList.getAliases(identifier);
|
||||
if(!aliases.isEmpty())
|
||||
{
|
||||
sb.append(aliases.get(0).getGroup());
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the System Label from the Audio Recording Alias
|
||||
* If this is a PatchGroup we return only the first sytem as the primary talkgroup system.
|
||||
*
|
||||
*/
|
||||
private String getSystemLabel(AudioRecording audioRecording)
|
||||
{
|
||||
List<Identifier> systems = audioRecording.getIdentifierCollection().getIdentifiers(Form.SYSTEM);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if(!systems.isEmpty())
|
||||
{
|
||||
sb.append(systems.get(0));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a formatted string with the patched talkgroups
|
||||
*/
|
||||
public static String getPatches(AudioRecording audioRecording)
|
||||
{
|
||||
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
|
||||
|
||||
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
|
||||
{
|
||||
PatchGroup patchGroup = patchGroupIdentifier.getValue();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("[");
|
||||
|
||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
return "[]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests both the connection and configuration against the OpenMHz Call API service
|
||||
* @param configuration containing API key and system id
|
||||
* @return error string or null if test is successful
|
||||
*/
|
||||
public static String testConnection(OpenMHzConfiguration configuration)
|
||||
{
|
||||
String uri = configuration.getHost() + "/" + configuration.getSystemName() + "/authorize";
|
||||
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.version(HttpClient.Version.HTTP_1_1)
|
||||
.followRedirects(HttpClient.Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(uri))
|
||||
.header(HttpHeaders.CONTENT_TYPE, APPLICATION_FORM_URLENCODED)
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("api_key="+configuration.getApiKey()))
|
||||
.build();
|
||||
|
||||
HttpResponse.BodyHandler<String> responseHandler = HttpResponse.BodyHandlers.ofString();
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponse<String> response = httpClient.send(request, responseHandler);
|
||||
|
||||
if (response.statusCode() == 200)
|
||||
{
|
||||
return "OK";
|
||||
}
|
||||
else if(response.statusCode() == 403)
|
||||
{
|
||||
return "Invalid API Key";
|
||||
}
|
||||
else if(response.statusCode() == 500)
|
||||
{
|
||||
return "Invalid System Name";
|
||||
}
|
||||
|
||||
return "No Response";
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Throwable throwableCause = e.getCause();
|
||||
|
||||
if(throwableCause instanceof ConnectException)
|
||||
{
|
||||
return "No Response";
|
||||
}
|
||||
|
||||
mLog.error("Exception connecting to OpenMHz server [" + e.toString() + "]");
|
||||
return "Uknown Exception";
|
||||
}
|
||||
}
|
||||
|
||||
public class AudioRecordingProcessor implements Runnable
|
||||
{
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
processRecordingQueue();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
|
||||
OpenMHzConfiguration config = new OpenMHzConfiguration();
|
||||
config.setHost("https://api.OpenMHz.com/call-upload-dev");
|
||||
config.setApiKey("c33aae37-8572-11ea-bd8b-0ecc8ab9ccec");
|
||||
config.setSystemName("systemx");
|
||||
|
||||
String response = testConnection(config);
|
||||
mLog.error("Response: " + response);
|
||||
|
||||
if(response == "OK")
|
||||
{
|
||||
mLog.debug("Test Successful!");
|
||||
}
|
||||
else
|
||||
{
|
||||
if(response.contains("Invalid API Key"))
|
||||
{
|
||||
mLog.error("Invalid API Key");
|
||||
}
|
||||
else if(response.contains("Invalid System Name"))
|
||||
{
|
||||
mLog.error("Invalid System Name");
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.debug("Response: " + response);
|
||||
}
|
||||
}
|
||||
|
||||
mLog.debug("Finished!");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import java.net.http.HttpRequest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.lang.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Builder for an HTTP body publisher to produce a OpenMHz call event
|
||||
*/
|
||||
public class OpenMHzBuilder
|
||||
{
|
||||
private static final String DASH_DASH = "--";
|
||||
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||
private List<Part> mParts = new ArrayList<>();
|
||||
private byte[] audioBytes = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*/
|
||||
public OpenMHzBuilder()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the static multi-part boundary string
|
||||
*/
|
||||
public String getBoundary()
|
||||
{
|
||||
return BOUNDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Audio bytes part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addFile(byte[] value)
|
||||
{
|
||||
audioBytes = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a string part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addPart(FormField key, String value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a number part to the call
|
||||
*/
|
||||
public OpenMHzBuilder addPart(FormField key, Number value)
|
||||
{
|
||||
if(key != null && value != null)
|
||||
{
|
||||
mParts.add(new Part(key.getHeader(), value.toString()));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a form-data item
|
||||
*/
|
||||
private static String formatPart(Part part, String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append(part.mKey).append("\"\r\n\r\n");
|
||||
sb.append(part.getValue()).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the audio file item
|
||||
*/
|
||||
private String formatFilePart(String boundary)
|
||||
{
|
||||
|
||||
if(audioBytes == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
StringBuilder sb= new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append("call").append("\"; filename=\"audio.mp3\"\r\n\r\n");
|
||||
return sb.toString();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the boundary closing item
|
||||
*/
|
||||
private static String getClosingBoundary(String boundary)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append(DASH_DASH).append("\r\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a BodyPublisher for accessing the call form data
|
||||
*/
|
||||
public HttpRequest.BodyPublisher build()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(formatFilePart(BOUNDARY));
|
||||
|
||||
|
||||
/**
|
||||
* We need to create a ByteArray consisting of the Sting "parts" and the audio file bytes
|
||||
*/
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
outputStream.write(sb.toString().getBytes());
|
||||
|
||||
if(audioBytes != null)
|
||||
{
|
||||
outputStream.write(audioBytes);
|
||||
}
|
||||
|
||||
sb = new StringBuilder();
|
||||
sb.append("\r\n");
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
outputStream.write(sb.toString().getBytes());
|
||||
}
|
||||
catch(IOException e)
|
||||
{
|
||||
//mLog.error("Rdio Scanner API - unable to create POST reqeust.");
|
||||
}
|
||||
|
||||
return HttpRequest.BodyPublishers.ofByteArray(outputStream.toByteArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for(Part part: mParts)
|
||||
{
|
||||
sb.append(formatPart(part, BOUNDARY));
|
||||
}
|
||||
|
||||
sb.append(getClosingBoundary(BOUNDARY));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key:Value pair holder
|
||||
*/
|
||||
public class Part
|
||||
{
|
||||
private String mKey;
|
||||
private String mValue;
|
||||
|
||||
/**
|
||||
* Constructs a new part
|
||||
* @param key value
|
||||
* @param value item
|
||||
*/
|
||||
public Part(String key, String value)
|
||||
{
|
||||
mKey = key;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
public String getKey()
|
||||
{
|
||||
return mKey;
|
||||
}
|
||||
|
||||
public String getValue()
|
||||
{
|
||||
return mValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
/**
|
||||
* Streaming configuration for Rdio Scanner API.
|
||||
*
|
||||
* Note: this API is not a streaming audio service, rather a completed call push service. However, it fits nicely
|
||||
* with the structure of the audio streaming subsystem in sdrtrunk
|
||||
*/
|
||||
public class OpenMHzConfiguration extends BroadcastConfiguration
|
||||
{
|
||||
public static final String PRODUCTION_ENDPOINT = "https://api.openmhz.com";
|
||||
|
||||
private StringProperty mSystemName = new SimpleStringProperty();
|
||||
private StringProperty mApiKey = new SimpleStringProperty();
|
||||
|
||||
/**
|
||||
* Constructor for faster jackson
|
||||
*/
|
||||
public OpenMHzConfiguration()
|
||||
{
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public constructor.
|
||||
* @param format to use for audio recording (MP3)
|
||||
*/
|
||||
public OpenMHzConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
if(getHost() == null)
|
||||
{
|
||||
setHost(PRODUCTION_ENDPOINT);
|
||||
}
|
||||
|
||||
//The parent class binds this property, so we unbind it and rebind it here
|
||||
mValid.unbind();
|
||||
mValid.bind(Bindings.and(Bindings.and(Bindings.isNotNull(mSystemName), Bindings.isNotNull(mApiKey)),
|
||||
Bindings.isNotNull(mHost)));
|
||||
}
|
||||
|
||||
/**
|
||||
* System ID as a property
|
||||
*/
|
||||
public StringProperty systemIDProperty()
|
||||
{
|
||||
return mSystemName;
|
||||
}
|
||||
|
||||
/**
|
||||
* API key as a property
|
||||
*/
|
||||
public StringProperty apiKeyProperty()
|
||||
{
|
||||
return mApiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "api_key")
|
||||
public String getApiKey()
|
||||
{
|
||||
return mApiKey.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the api key
|
||||
* @param apiKey
|
||||
*/
|
||||
public void setApiKey(String apiKey)
|
||||
{
|
||||
mApiKey.setValue(apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* System name as provided by OpenMHz.com
|
||||
*/
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "system_name")
|
||||
public String getSystemName()
|
||||
{
|
||||
return mSystemName.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system name provided by OpenMHz.com
|
||||
*/
|
||||
public void setSystemName(String systemName)
|
||||
{
|
||||
mSystemName.set(systemName);
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
OpenMHzConfiguration copy = new OpenMHzConfiguration();
|
||||
copy.setSystemName(getSystemName());
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.gui.playlist.streaming;
|
||||
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
|
||||
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
|
||||
import io.github.dsheirer.gui.control.IntegerTextField;
|
||||
import io.github.dsheirer.playlist.PlaylistManager;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.controlsfx.control.ToggleSwitch;
|
||||
|
||||
/**
|
||||
* OpenMHz calls API configuration editor
|
||||
*/
|
||||
public class OpenMHzEditor extends AbstractBroadcastEditor<OpenMHzConfiguration>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzEditor.class);
|
||||
private TextField mSystemNameTextField;
|
||||
private IntegerTextField mMaxAgeTextField;
|
||||
private TextField mApiKeyTextField;
|
||||
private TextField mHostTextField;
|
||||
private GridPane mEditorPane;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param playlistManager for accessing the broadcast model
|
||||
*/
|
||||
public OpenMHzEditor(PlaylistManager playlistManager)
|
||||
{
|
||||
super(playlistManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setItem(OpenMHzConfiguration item)
|
||||
{
|
||||
super.setItem(item);
|
||||
|
||||
getSystemNameTextField().setDisable(item == null);
|
||||
getApiKeyTextField().setDisable(item == null);
|
||||
getHostTextField().setDisable(item == null);
|
||||
getMaxAgeTextField().setDisable(item == null);
|
||||
|
||||
if(item != null)
|
||||
{
|
||||
getSystemNameTextField().setText(item.getSystemName());
|
||||
getApiKeyTextField().setText(item.getApiKey());
|
||||
getHostTextField().setText(item.getHost());
|
||||
getMaxAgeTextField().set((int)(item.getMaximumRecordingAge() / 1000));
|
||||
}
|
||||
else
|
||||
{
|
||||
getSystemNameTextField().setText(null);
|
||||
getApiKeyTextField().setText(null);
|
||||
getHostTextField().setText(null);
|
||||
getMaxAgeTextField().set(0);
|
||||
}
|
||||
|
||||
modifiedProperty().set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save()
|
||||
{
|
||||
if(getItem() != null)
|
||||
{
|
||||
getItem().setSystemName(getSystemNameTextField().getText());
|
||||
getItem().setHost(getHostTextField().getText());
|
||||
getItem().setApiKey(getApiKeyTextField().getText());
|
||||
getItem().setMaximumRecordingAge(getMaxAgeTextField().get() * 1000);
|
||||
}
|
||||
|
||||
super.save();
|
||||
}
|
||||
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
protected GridPane getEditorPane()
|
||||
{
|
||||
if(mEditorPane == null)
|
||||
{
|
||||
mEditorPane = new GridPane();
|
||||
mEditorPane.setPadding(new Insets(10, 5, 10,10));
|
||||
mEditorPane.setVgap(10);
|
||||
mEditorPane.setHgap(5);
|
||||
|
||||
int row = 0;
|
||||
|
||||
Label formatLabel = new Label("Format");
|
||||
GridPane.setHalignment(formatLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(formatLabel, 0, row);
|
||||
mEditorPane.getChildren().add(formatLabel);
|
||||
|
||||
GridPane.setConstraints(getFormatField(), 1, row);
|
||||
mEditorPane.getChildren().add(getFormatField());
|
||||
|
||||
Label enabledLabel = new Label("Enabled");
|
||||
GridPane.setHalignment(enabledLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(enabledLabel, 2, row);
|
||||
mEditorPane.getChildren().add(enabledLabel);
|
||||
|
||||
GridPane.setConstraints(getEnabledSwitch(), 3, row);
|
||||
mEditorPane.getChildren().add(getEnabledSwitch());
|
||||
|
||||
Label systemLabel = new Label("Name");
|
||||
GridPane.setHalignment(systemLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemLabel);
|
||||
|
||||
GridPane.setConstraints(getNameTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getNameTextField());
|
||||
|
||||
Label apiKeyLabel = new Label("API Key");
|
||||
GridPane.setHalignment(apiKeyLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(apiKeyLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(apiKeyLabel);
|
||||
|
||||
GridPane.setConstraints(getApiKeyTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getApiKeyTextField());
|
||||
|
||||
Label systemNameLabel = new Label("System Short Name");
|
||||
GridPane.setHalignment(systemNameLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(systemNameLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(systemNameLabel);
|
||||
|
||||
GridPane.setConstraints(getSystemNameTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getSystemNameTextField());
|
||||
|
||||
Label hostLabel = new Label("OpenMHz Upload Server (API)");
|
||||
GridPane.setHalignment(hostLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(hostLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(hostLabel);
|
||||
|
||||
GridPane.setConstraints(getHostTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getHostTextField());
|
||||
|
||||
Label maxAgeLabel = new Label("Max Recording Age (seconds)");
|
||||
GridPane.setHalignment(maxAgeLabel, HPos.RIGHT);
|
||||
GridPane.setConstraints(maxAgeLabel, 0, ++row);
|
||||
mEditorPane.getChildren().add(maxAgeLabel);
|
||||
|
||||
GridPane.setConstraints(getMaxAgeTextField(), 1, row);
|
||||
mEditorPane.getChildren().add(getMaxAgeTextField());
|
||||
|
||||
}
|
||||
|
||||
return mEditorPane;
|
||||
}
|
||||
|
||||
private IntegerTextField getMaxAgeTextField()
|
||||
{
|
||||
if(mMaxAgeTextField == null)
|
||||
{
|
||||
mMaxAgeTextField = new IntegerTextField();
|
||||
mMaxAgeTextField.setDisable(true);
|
||||
mMaxAgeTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mMaxAgeTextField;
|
||||
}
|
||||
|
||||
private TextField getHostTextField()
|
||||
{
|
||||
if(mHostTextField == null)
|
||||
{
|
||||
mHostTextField = new TextField();
|
||||
mHostTextField.setDisable(true);
|
||||
mHostTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mHostTextField;
|
||||
}
|
||||
|
||||
private TextField getApiKeyTextField()
|
||||
{
|
||||
if(mApiKeyTextField == null)
|
||||
{
|
||||
mApiKeyTextField = new TextField();
|
||||
mApiKeyTextField.setDisable(true);
|
||||
mApiKeyTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mApiKeyTextField;
|
||||
}
|
||||
|
||||
private TextField getSystemNameTextField()
|
||||
{
|
||||
if(mSystemNameTextField == null)
|
||||
{
|
||||
mSystemNameTextField = new TextField();
|
||||
mSystemNameTextField.setDisable(true);
|
||||
mSystemNameTextField.textProperty().addListener(mEditorModificationListener);
|
||||
}
|
||||
|
||||
return mSystemNameTextField;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
*
|
||||
*
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast.openmhz;
|
||||
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastConfiguration;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastFormat;
|
||||
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastTCPConfiguration;
|
||||
import io.github.dsheirer.rrapi.type.UserFeedBroadcast;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class OpenMHzFeedConfiguration extends IcecastTCPConfiguration
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OpenMHzFeedConfiguration.class);
|
||||
|
||||
private int mFeedID;
|
||||
|
||||
public OpenMHzFeedConfiguration()
|
||||
{
|
||||
//No-arg constructor for JAXB
|
||||
this(BroadcastFormat.MP3);
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenMHz configuration for Icecast 2.3.2 compatible servers
|
||||
*
|
||||
* @param format of output audio (MP3)
|
||||
*/
|
||||
public OpenMHzFeedConfiguration(BroadcastFormat format)
|
||||
{
|
||||
super(format);
|
||||
|
||||
setBitRate(16);
|
||||
setChannels(1);
|
||||
setSampleRate(8000);
|
||||
setInline(true);
|
||||
}
|
||||
|
||||
public static OpenMHzFeedConfiguration from(UserFeedBroadcast userFeedBroadcast)
|
||||
{
|
||||
OpenMHzFeedConfiguration config = new OpenMHzFeedConfiguration(BroadcastFormat.MP3);
|
||||
config.setName(userFeedBroadcast.getDescription());
|
||||
config.setHost(userFeedBroadcast.getHostname());
|
||||
config.setMountPoint(userFeedBroadcast.getMount());
|
||||
config.setFeedID(userFeedBroadcast.getFeedId());
|
||||
config.setPassword(userFeedBroadcast.getPassword());
|
||||
|
||||
try
|
||||
{
|
||||
config.setPort(Integer.parseInt(userFeedBroadcast.getPort()));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error creating Rdio Scanner configuration from radio reference user feed instance");
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BroadcastConfiguration copyOf()
|
||||
{
|
||||
OpenMHzFeedConfiguration copy = new OpenMHzFeedConfiguration(getBroadcastFormat());
|
||||
|
||||
//Broadcast Configuration Parameters
|
||||
copy.setName(getName());
|
||||
copy.setHost(getHost());
|
||||
copy.setPort(getPort());
|
||||
copy.setInline(getInline());
|
||||
copy.setPassword(getPassword());
|
||||
copy.setDelay(getDelay());
|
||||
copy.setEnabled(false);
|
||||
|
||||
//Icecast Configuration Parameters
|
||||
copy.setUserName(getUserName());
|
||||
copy.setMountPoint(getMountPoint());
|
||||
copy.setDescription(getDescription());
|
||||
copy.setGenre(getGenre());
|
||||
copy.setPublic(isPublic());
|
||||
copy.setURL(getURL());
|
||||
|
||||
//OpenMHz Configuration Parameters
|
||||
copy.setFeedID(getFeedID());
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "type", namespace = "http://www.w3.org/2001/XMLSchema-instance")
|
||||
@Override
|
||||
public BroadcastServerType getBroadcastServerType()
|
||||
{
|
||||
return BroadcastServerType.OPENMHZ;
|
||||
}
|
||||
|
||||
@JacksonXmlProperty(isAttribute = true, localName = "feed_id")
|
||||
public int getFeedID()
|
||||
{
|
||||
return mFeedID;
|
||||
}
|
||||
|
||||
public void setFeedID(int feedID)
|
||||
{
|
||||
mFeedID = feedID;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,7 +20,6 @@
|
||||
package io.github.dsheirer.audio.broadcast.rdioscanner;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.base.Joiner;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
@@ -34,7 +33,6 @@ import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
|
||||
import io.github.dsheirer.identifier.patch.PatchGroup;
|
||||
@@ -42,24 +40,12 @@ import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
|
||||
import io.github.dsheirer.identifier.radio.RadioIdentifier;
|
||||
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
@@ -67,6 +53,8 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
/**
|
||||
@@ -244,6 +232,9 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
String talkgroupLabel = getTalkgroupLabel(audioRecording);
|
||||
String talkgroupGroup = getTalkgroupGroup(audioRecording);
|
||||
String systemLabel = getSystemLabel(audioRecording);
|
||||
String path = audioRecording.getPath().toString();
|
||||
// Remove TEMPORARY_STREAM_FILE_SUFFIX
|
||||
String audioName = path.substring(path.substring(0, path.lastIndexOf("_")).lastIndexOf("_") + 1);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -264,6 +255,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
RdioScannerBuilder bodyBuilder = new RdioScannerBuilder();
|
||||
bodyBuilder.addPart(FormField.KEY, getBroadcastConfiguration().getApiKey())
|
||||
.addPart(FormField.SYSTEM, getBroadcastConfiguration().getSystemID())
|
||||
.addAudioName(audioName)
|
||||
.addFile(audioBytes)
|
||||
.addPart(FormField.DATE_TIME, timestampSeconds)
|
||||
.addPart(FormField.TALKGROUP_ID, talkgroup)
|
||||
@@ -278,7 +270,6 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
.uri(URI.create(getBroadcastConfiguration().getHost()))
|
||||
.header(HttpHeaders.CONTENT_TYPE, MULTIPART_FORM_DATA + "; boundary=" + bodyBuilder.getBoundary())
|
||||
.header(HttpHeaders.USER_AGENT, "sdrtrunk")
|
||||
.header(HttpHeaders.CONTENT_TYPE, "audio/mpeg")
|
||||
.POST(bodyBuilder.build())
|
||||
.build();
|
||||
|
||||
@@ -525,7 +516,12 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
|
||||
|
||||
sb.append(patchGroup.getPatchGroup().getValue().toString());
|
||||
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedGroupIdentifiers())
|
||||
for(TalkgroupIdentifier patched: patchGroup.getPatchedTalkgroupIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
for(RadioIdentifier patched: patchGroup.getPatchedRadioIdentifiers())
|
||||
{
|
||||
sb.append(",").append(patched.getValue());
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ public class RdioScannerBuilder
|
||||
private static final String DASH_DASH = "--";
|
||||
private static final String BOUNDARY = "--sdrtrunk-sdrtrunk-sdrtrunk";
|
||||
private List<Part> mParts = new ArrayList<>();
|
||||
private byte[] audioBytes = null;
|
||||
private byte[] audioBytes = null;
|
||||
private String audioName = null;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -51,6 +52,15 @@ public class RdioScannerBuilder
|
||||
return BOUNDARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Audio file name part to the call
|
||||
*/
|
||||
public RdioScannerBuilder addAudioName(String value)
|
||||
{
|
||||
audioName = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Audio bytes part to the call
|
||||
*/
|
||||
@@ -110,7 +120,8 @@ public class RdioScannerBuilder
|
||||
}
|
||||
StringBuilder sb= new StringBuilder();
|
||||
sb.append(DASH_DASH).append(boundary).append("\r\n");
|
||||
sb.append("Content-Disposition: form-data; name=\"").append("audio").append("\"\r\n\r\n");
|
||||
sb.append("Content-Disposition: form-data; filename=\"").append(audioName);
|
||||
sb.append("\"; name=\"").append("audio").append("\"\r\n\r\n");
|
||||
return sb.toString();
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -24,9 +24,7 @@ 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;
|
||||
@@ -62,6 +60,11 @@ public class RdioScannerConfiguration extends BroadcastConfiguration
|
||||
mValid.unbind();
|
||||
mValid.bind(Bindings.and(Bindings.and(Bindings.greaterThan(mSystemID, 0), Bindings.isNotNull(mApiKey)),
|
||||
Bindings.isNotNull(mHost)));
|
||||
|
||||
if(mHost.getValue() == null || mHost.getValue().isEmpty())
|
||||
{
|
||||
mHost.set("http://localhost");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -41,6 +41,9 @@ import jmbe.iface.IAudioCodecLibrary;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Base audio module for protocols that use the JMBE audio codec.
|
||||
*/
|
||||
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
|
||||
ISquelchStateListener
|
||||
{
|
||||
@@ -59,12 +62,23 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
|
||||
loadConverter();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeAudioSegment()
|
||||
{
|
||||
super.closeAudioSegment();
|
||||
|
||||
//Reset the audio codec to clear any leftover frame data from the previous call.
|
||||
if(mAudioCodec != null)
|
||||
{
|
||||
mAudioCodec.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose()
|
||||
{
|
||||
super.dispose();
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
mAudioCodec = null;
|
||||
}
|
||||
|
||||
public IAudioCodec getAudioCodec()
|
||||
|
||||
@@ -125,7 +125,7 @@ public class MBECallSequenceConverter
|
||||
|
||||
try
|
||||
{
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath);
|
||||
AudioSegmentRecorder.recordWAVE(audioSegment, outputPath, audioSegment.getIdentifierCollection());
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -25,6 +25,7 @@ import io.github.dsheirer.controller.NamingThreadFactory;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
@@ -58,6 +59,7 @@ import javax.sound.sampled.SourceDataLine;
|
||||
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
|
||||
private int mBufferStartThreshold;
|
||||
private int mBufferStopThreshold;
|
||||
private Listener<IdentifierCollection> mIdentifierCollectionListener;
|
||||
@@ -82,6 +84,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
private boolean mDropDuplicates;
|
||||
private long mOutputLastTimestamp = 0;
|
||||
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
|
||||
private AudioFormat mAudioFormat;
|
||||
private Line.Info mLineInfo;
|
||||
private int mRequestedBufferSize;
|
||||
|
||||
/**
|
||||
* Single audio channel playback with automatic starting and stopping of the
|
||||
@@ -105,11 +110,14 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
|
||||
"sdrtrunk audio output " + mixerChannel.name()));
|
||||
mUserPreferences = userPreferences;
|
||||
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mAudioFormat = audioFormat;
|
||||
mLineInfo = lineInfo;
|
||||
mRequestedBufferSize = requestedBufferSize;
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = (SourceDataLine) mMixer.getLine(lineInfo);
|
||||
mOutput = (SourceDataLine) mMixer.getLine(mLineInfo);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
@@ -132,8 +140,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" +
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
|
||||
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -143,8 +152,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" +
|
||||
mixer.getMixerInfo().getName() + " | " + getChannelName() + "]");
|
||||
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
|
||||
"MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
}
|
||||
|
||||
//Run the queue processor task every 100 milliseconds or 10 times a second
|
||||
@@ -230,7 +240,7 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
}
|
||||
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
|
||||
{
|
||||
mDropDuplicates = mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,6 +276,12 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
*/
|
||||
private void playAudio(ByteBuffer buffer)
|
||||
{
|
||||
if(mOutput == null)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request");
|
||||
return;
|
||||
}
|
||||
|
||||
if(buffer != null)
|
||||
{
|
||||
int wrote = 0;
|
||||
@@ -286,6 +302,105 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
|
||||
mOutputLastTimestamp = System.currentTimeMillis();
|
||||
|
||||
checkStart();
|
||||
|
||||
//Something is causing the source data line to fail to accept audio byte data via the write() method
|
||||
//and this seems to have started around JDK22, maybe. We can detect when this happens and close() and
|
||||
//reopen the data line to clear the error.
|
||||
if(!mOutput.isRunning() && wrote <= 0 && buffer.array().length > 0)
|
||||
{
|
||||
mOutput.close();
|
||||
|
||||
try
|
||||
{
|
||||
mOutput = (SourceDataLine)mMixer.getLine(mLineInfo);
|
||||
|
||||
if(mOutput != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
mGainControl = (FloatControl) gain;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
|
||||
"MASTER GAIN control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
|
||||
"MASTER MUTE control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
|
||||
getChannelName() + "]");
|
||||
}
|
||||
|
||||
LOGGING_SUPPRESSOR.info("reopen audio output success", 2,
|
||||
"Closed and reopened audio output - success - mOutput is not null");
|
||||
}
|
||||
else
|
||||
{
|
||||
LOGGING_SUPPRESSOR.info("reopen audio output fail", 2,
|
||||
"Closed and reopened audio output - fail - mOutput is null");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch(LineUnavailableException lue)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("reopen fail for lua", 2, "Attempt to reopen " +
|
||||
"audio source data line failed", lue);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
mOutput.open(mAudioFormat, mRequestedBufferSize);
|
||||
|
||||
//Start threshold: buffer is full with 10% or less of capacity remaining
|
||||
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
|
||||
|
||||
//Stop threshold: buffer is empty with 90% or more capacity available
|
||||
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
|
||||
|
||||
try
|
||||
{
|
||||
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
|
||||
mGainControl = (FloatControl) gain;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" + getChannelName() +
|
||||
"] after reopening the source data line");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
|
||||
mMuteControl = (BooleanControl) mute;
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.warn("Couldn't obtain MUTE control for stereo line [" + getChannelName() +
|
||||
"] after reopening the source data line");
|
||||
}
|
||||
|
||||
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
|
||||
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
|
||||
mCanProcessAudio = true;
|
||||
}
|
||||
catch(LineUnavailableException e)
|
||||
{
|
||||
LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 2,
|
||||
"Failed to (re)open the source data line for audio output - mixer [" +
|
||||
mMixer.getMixerInfo().getName() + "]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(mOutput.isRunning() && wrote < buffer.array().length)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -41,6 +41,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -65,6 +66,8 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
|
||||
private ScheduledExecutorService mScheduledExecutorService =
|
||||
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
|
||||
private AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
|
||||
private ReentrantLock mAudioOutputLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* Constructs an instance.
|
||||
@@ -95,6 +98,9 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
mLog.warn("No audio output devices available");
|
||||
}
|
||||
|
||||
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,7 +126,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
while(newSegment != null)
|
||||
{
|
||||
if(newSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
{
|
||||
newSegment.decrementConsumerCount();
|
||||
}
|
||||
@@ -148,7 +154,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled())
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
@@ -182,43 +188,60 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.isDoNotMonitor() || (audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
}
|
||||
else if(audioSegment.isLinked())
|
||||
{
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
mAudioOutputLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
it.remove();
|
||||
audioOutput.play(audioSegment);
|
||||
if(audioOutput.isLinkedTo(audioSegment))
|
||||
{
|
||||
it.remove();
|
||||
audioOutput.play(audioSegment);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Sort audio segments by playback priority and assign to empty audio outputs
|
||||
if(!mAudioSegments.isEmpty())
|
||||
{
|
||||
//TODO: change this to take audio segment start time into account also
|
||||
mAudioSegments.sort(Comparator.comparingInt(o -> o.monitorPriorityProperty().get()));
|
||||
mAudioSegments.sort(mAudioSegmentPrioritySorter);
|
||||
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
mAudioOutputLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
if(audioOutput.isEmpty())
|
||||
//Assign empty audio outputs first
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
if(audioOutput.isEmpty())
|
||||
{
|
||||
return;
|
||||
audioOutput.play(mAudioSegments.remove(0));
|
||||
|
||||
if(mAudioSegments.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
//Remove any audio segments marked as complete that didn't get assigned to an output
|
||||
@@ -228,7 +251,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
audioSegment = it.next();
|
||||
|
||||
if(audioSegment.completeProperty().get() || (audioSegment.isDuplicate() &&
|
||||
mUserPreferences.getDuplicateCallDetectionPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled()))
|
||||
{
|
||||
it.remove();
|
||||
audioSegment.decrementConsumerCount();
|
||||
@@ -288,37 +311,39 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
{
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
|
||||
|
||||
if(mProcessingTask != null)
|
||||
mAudioOutputLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mProcessingTask.cancel(true);
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputs.clear();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||
mAudioOutputs.add(mono);
|
||||
break;
|
||||
case STEREO:
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
||||
mAudioOutputs.add(left);
|
||||
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
||||
mAudioOutputs.add(right);
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mAudioOutputLock.unlock();
|
||||
}
|
||||
|
||||
for(AudioOutput audioOutput: mAudioOutputs)
|
||||
{
|
||||
audioOutput.dispose();
|
||||
}
|
||||
|
||||
mAudioOutputs.clear();
|
||||
|
||||
switch(entry.getMixerChannel())
|
||||
{
|
||||
case MONO:
|
||||
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
|
||||
mAudioOutputs.add(mono);
|
||||
break;
|
||||
case STEREO:
|
||||
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
|
||||
mAudioOutputs.add(left);
|
||||
|
||||
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
|
||||
mAudioOutputs.add(right);
|
||||
break;
|
||||
default:
|
||||
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
|
||||
}
|
||||
|
||||
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
|
||||
0, 100, TimeUnit.MILLISECONDS);
|
||||
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
|
||||
mMixerChannelConfiguration = entry;
|
||||
}
|
||||
@@ -389,4 +414,30 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio segment comparator for sorting audio segments by: 1)Playback priority and 2)Segment start time
|
||||
*/
|
||||
public class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
|
||||
{
|
||||
@Override
|
||||
public int compare(AudioSegment segment1, AudioSegment segment2)
|
||||
{
|
||||
if(segment1 == null || segment2 == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
//If priority is the same, sort by start time
|
||||
if(segment1.monitorPriorityProperty().get() == segment2.monitorPriorityProperty().get())
|
||||
{
|
||||
return Long.compare(segment1.getStartTimestamp(), segment2.getStartTimestamp());
|
||||
}
|
||||
//Otherwise, sort by priority
|
||||
else
|
||||
{
|
||||
return Integer.compare(segment1.monitorPriorityProperty().get(), segment2.monitorPriorityProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -20,6 +20,7 @@ package io.github.dsheirer.bits;
|
||||
|
||||
import io.github.dsheirer.edac.CRC;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.BitSet;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
@@ -276,8 +277,7 @@ public class BinaryMessage extends BitSet
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new BitSetFullException("bitset is full -- contains " +
|
||||
(mPointer + 1) + "bits");
|
||||
throw new BitSetFullException("bitset is full -- contains [" + mPointer + "/" + size() + "] bits");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,8 +307,8 @@ public class BinaryMessage extends BitSet
|
||||
|
||||
/**
|
||||
* Overrides the parent method which returns a BitSet so that we can return a BinaryMessage
|
||||
* @param from
|
||||
* @param to
|
||||
* @param from inclusive
|
||||
* @param to exclusive
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@@ -451,6 +451,203 @@ public class BinaryMessage extends BitSet
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(IntField intField)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index = intField.start(); index <= intField.end(); index++)
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument.
|
||||
* @param fragmentedField with an array of message indices
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(FragmentedIntField fragmentedField)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param intField to inspect
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(IntField intField)
|
||||
{
|
||||
return nextSetBit(intField.start()) <= intField.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param fragmentedField to inspect
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(FragmentedIntField fragmentedField)
|
||||
{
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
if(get(index))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument where the message start index is
|
||||
* offset within this binary message.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @param offset to the start of the first message index.
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(IntField intField, int offset)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer value of the message field described by the field argument where the message start index is
|
||||
* offset within this binary message.
|
||||
* @param fragmentedField with an array of field indices
|
||||
* @param offset to the start of the first message index.
|
||||
* @return integer field value.
|
||||
*/
|
||||
public int getInt(FragmentedIntField fragmentedField, int offset)
|
||||
{
|
||||
int value = 0;
|
||||
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
value = Integer.rotateLeft(value, 1);
|
||||
|
||||
if(get(index + offset))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param intField to inspect
|
||||
* @param offset to the first bit of the message.
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(IntField intField, int offset)
|
||||
{
|
||||
return nextSetBit(intField.start() + offset) <= (intField.end() + offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the message field contains a non-zero value indicated by any bits set in the int field indices.
|
||||
* @param intField to inspect
|
||||
* @param offset to the first bit of the message.
|
||||
* @return true if any of the bits are set, indicating a non-zero value.
|
||||
*/
|
||||
public boolean hasInt(FragmentedIntField fragmentedField, int offset)
|
||||
{
|
||||
for(int index: fragmentedField.indices())
|
||||
{
|
||||
if(get(index + offset))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the long value of the message field described by the field argument.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @return integer field value.
|
||||
*/
|
||||
public long getLong(LongField intField)
|
||||
{
|
||||
long value = 0;
|
||||
|
||||
for(int index = intField.start(); index <= intField.end(); index++)
|
||||
{
|
||||
value = Long.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the long value of the message field described by the field argument where the message start index is
|
||||
* offset within this binary message.
|
||||
* @param intField with start and end indices (inclusive).
|
||||
* @param offset to the start of the first message index.
|
||||
* @return field value.
|
||||
*/
|
||||
public long getLong(LongField intField, int offset)
|
||||
{
|
||||
long value = 0;
|
||||
|
||||
for(int index = intField.start() + offset; index <= intField.end() + offset; index++)
|
||||
{
|
||||
value = Long.rotateLeft(value, 1);
|
||||
|
||||
if(get(index))
|
||||
{
|
||||
value++;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the integer value represented by the bit array
|
||||
*
|
||||
@@ -742,6 +939,18 @@ public class BinaryMessage extends BitSet
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the integer field formatted as a hex value using zero prefixes to pad the hex character count to fully
|
||||
* represent the size (width) of the field.
|
||||
* @param field to parse as hex
|
||||
* @return hex value.
|
||||
*/
|
||||
public String getHex(IntField field)
|
||||
{
|
||||
int width = Math.ceilDiv(field.width(), 4);
|
||||
return String.format("%0" + width + "X", getInt(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less
|
||||
* than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in
|
||||
@@ -845,13 +1054,18 @@ public class BinaryMessage extends BitSet
|
||||
*/
|
||||
public int getTwosComplement(int start, int end)
|
||||
{
|
||||
BinaryMessage sub = getSubMessage(start, end);
|
||||
boolean negative = sub.get(start);
|
||||
sub.flip(0, sub.size());
|
||||
|
||||
int value = sub.getInt(0, sub.size()) + 1;
|
||||
value *= (negative ? -1 : 1);
|
||||
return value;
|
||||
if(get(start))
|
||||
{
|
||||
//Negative value - flip and add one
|
||||
BinaryMessage fragment = getSubMessage(start, end + 1);
|
||||
fragment.flip(0, fragment.size());
|
||||
return -(fragment.getInt(0, fragment.size() - 1) + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
//Positive value - return the contents.
|
||||
return getInt(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1193,9 +1407,22 @@ public class BinaryMessage extends BitSet
|
||||
{
|
||||
byte[] bytes = new byte[characterCount];
|
||||
|
||||
int index;
|
||||
for(int x = 0; x < characterCount; x++)
|
||||
{
|
||||
bytes[x] = getByte(CHARACTER_7_BIT, x * 7 + offset);
|
||||
index = x * 7 + offset;
|
||||
|
||||
if(index == 0)
|
||||
{
|
||||
bytes[x] = getByte(CHARACTER_8_BIT, index);
|
||||
bytes[x] = (byte)((bytes[x] >> 1) & 0x7F);
|
||||
}
|
||||
else
|
||||
{
|
||||
index--;
|
||||
bytes[x] = getByte(CHARACTER_8_BIT, index);
|
||||
bytes[x] &= (byte)0x7F; //mask off the high bit since we have to parse it as an 8-bit byte.
|
||||
}
|
||||
}
|
||||
|
||||
return new String(bytes);
|
||||
@@ -1274,7 +1501,7 @@ public class BinaryMessage extends BitSet
|
||||
bytes[x] = getByte(CHARACTER_8_BIT, x * 8 + offset);
|
||||
}
|
||||
|
||||
return new String(bytes);
|
||||
return new String(bytes, Charset.forName("UTF-16"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
@@ -84,9 +81,9 @@ public class CorrectedBinaryMessage extends BinaryMessage
|
||||
/**
|
||||
* Returns a new binary message containing the bits from (inclusive) to end (exclusive).
|
||||
*
|
||||
* @param start bit
|
||||
* @param end bit
|
||||
* @return message
|
||||
* @param start bit inclusive
|
||||
* @param end bit exclusive
|
||||
* @return message with sub message bits
|
||||
*/
|
||||
public CorrectedBinaryMessage getSubMessage(int start, int end)
|
||||
{
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,41 +14,32 @@
|
||||
*
|
||||
* 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.module.decode.p25.phase1.message.filter;
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
import io.github.dsheirer.filter.Filter;
|
||||
import io.github.dsheirer.filter.FilterElement;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.p25.phase1.message.pdu.umbtc.UMBTCMessage;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class UMBTCMessageFilter extends Filter<IMessage>
|
||||
/**
|
||||
* Defines a fragmented or non-contiguous bit field within a binary message.
|
||||
* @param indices for the bits in the field.
|
||||
*/
|
||||
public record FragmentedIntField(int... indices)
|
||||
{
|
||||
public UMBTCMessageFilter()
|
||||
public FragmentedIntField
|
||||
{
|
||||
super("Unconfirmed Multi-Block Trunking Control");
|
||||
if(indices.length > 32)
|
||||
{
|
||||
throw new IllegalArgumentException("Integer field indices size [" + indices.length + "] cannot exceed 32-bits for an integer");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean passes(IMessage message)
|
||||
/**
|
||||
* Utility constructor method.
|
||||
* @param indices (inclusive)
|
||||
* @return constructed fragmented integer field.
|
||||
*/
|
||||
public static FragmentedIntField of(int... indices)
|
||||
{
|
||||
return mEnabled && canProcess(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canProcess(IMessage message)
|
||||
{
|
||||
return message instanceof UMBTCMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilterElement<?>> getFilterElements()
|
||||
{
|
||||
return Collections.EMPTY_LIST;
|
||||
return new FragmentedIntField(indices);
|
||||
}
|
||||
}
|
||||
131
src/main/java/io/github/dsheirer/bits/IntField.java
Normal file
131
src/main/java/io/github/dsheirer/bits/IntField.java
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
/**
|
||||
* Defines a contiguous bit field within a binary message.
|
||||
* @param start bit index, inclusive.
|
||||
* @param end bit index, inclusive.
|
||||
*/
|
||||
public record IntField(int start, int end)
|
||||
{
|
||||
public IntField
|
||||
{
|
||||
if(end - start > 32)
|
||||
{
|
||||
throw new IllegalArgumentException("Integer field length [" + (end - start) + "] cannot exceed 32-bits for an integer");
|
||||
}
|
||||
|
||||
if(end < start)
|
||||
{
|
||||
throw new IllegalArgumentException("Integer field start index must be less than end index");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the field in bit positions
|
||||
* @return width
|
||||
*/
|
||||
public int width()
|
||||
{
|
||||
return end() - start() + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method.
|
||||
* @param start index (inclusive)
|
||||
* @param end index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField range(int start, int end)
|
||||
{
|
||||
return new IntField(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one four bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length4(int start)
|
||||
{
|
||||
return new IntField(start, (start + 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one octet of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length8(int start)
|
||||
{
|
||||
return new IntField(start, (start + 7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 12 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length12(int start)
|
||||
{
|
||||
return new IntField(start, (start + 11));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length16(int start)
|
||||
{
|
||||
return new IntField(start, (start + 15));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 20 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length20(int start)
|
||||
{
|
||||
return new IntField(start, (start + 19));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length24(int start)
|
||||
{
|
||||
return new IntField(start, (start + 23));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with three octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static IntField length32(int start)
|
||||
{
|
||||
return new IntField(start, (start + 31));
|
||||
}
|
||||
}
|
||||
122
src/main/java/io/github/dsheirer/bits/LongField.java
Normal file
122
src/main/java/io/github/dsheirer/bits/LongField.java
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.bits;
|
||||
|
||||
/**
|
||||
* Defines a contiguous bit field within a binary message.
|
||||
* @param start bit index, inclusive.
|
||||
* @param end bit index, inclusive.
|
||||
*/
|
||||
public record LongField(int start, int end)
|
||||
{
|
||||
public LongField
|
||||
{
|
||||
if(end - start > 64)
|
||||
{
|
||||
throw new IllegalArgumentException("Long field length [" + (end - start) + "] cannot exceed 64-bits for a long");
|
||||
}
|
||||
|
||||
if(end < start)
|
||||
{
|
||||
throw new IllegalArgumentException("Long field start index must be less than end index");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method.
|
||||
* @param start index (inclusive)
|
||||
* @param end index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField range(int start, int end)
|
||||
{
|
||||
return new LongField(start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one four bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length4(int start)
|
||||
{
|
||||
return new LongField(start, (start + 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with one octet of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length8(int start)
|
||||
{
|
||||
return new LongField(start, (start + 7));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 12 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length12(int start)
|
||||
{
|
||||
return new LongField(start, (start + 11));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length16(int start)
|
||||
{
|
||||
return new LongField(start, (start + 15));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with 20 bits of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length20(int start)
|
||||
{
|
||||
return new LongField(start, (start + 19));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with two octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length24(int start)
|
||||
{
|
||||
return new LongField(start, (start + 23));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility constructor method for a field with three octets of length.
|
||||
* @param start index (inclusive)
|
||||
* @return constructed bit field.
|
||||
*/
|
||||
public static LongField length32(int start)
|
||||
{
|
||||
return new LongField(start, (start + 31));
|
||||
}
|
||||
}
|
||||
@@ -132,12 +132,10 @@ public enum SyncPattern
|
||||
true, true, false, false //1100 0xC
|
||||
}),
|
||||
|
||||
/* Sync (0x158) = 1101011000 */
|
||||
/* Sync (0x158) = 101011000 */
|
||||
PASSPORT( new boolean[]
|
||||
{
|
||||
true, //0001 0x1
|
||||
false, true, false, true, //0101 0x5
|
||||
true, false, false, false //1000 0x8
|
||||
true,false,true,false,true,true,false,false,false
|
||||
} ),
|
||||
|
||||
LTR_STANDARD_OSW( new boolean[]
|
||||
|
||||
@@ -22,6 +22,8 @@ package io.github.dsheirer.buffer;
|
||||
import io.github.dsheirer.sample.SampleUtils;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
@@ -30,6 +32,7 @@ import java.util.Iterator;
|
||||
public class FloatNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
private float[] mInterleavedComplexSamples;
|
||||
private static final int BUFFER_SIZE = 2048;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -81,48 +84,51 @@ public class FloatNativeBuffer extends AbstractNativeBuffer
|
||||
|
||||
private class ComplexSamplesIterator implements Iterator<ComplexSamples>
|
||||
{
|
||||
private boolean mEmpty;
|
||||
private int mBufferPointer = 0;
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
return !mEmpty;
|
||||
return mBufferPointer < mInterleavedComplexSamples.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComplexSamples next()
|
||||
{
|
||||
if(mEmpty)
|
||||
if(!hasNext())
|
||||
{
|
||||
throw new IllegalStateException("No more samples");
|
||||
}
|
||||
|
||||
mEmpty = true;
|
||||
return SampleUtils.deinterleave(mInterleavedComplexSamples, getTimestamp());
|
||||
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
|
||||
mBufferPointer += BUFFER_SIZE * 2;
|
||||
|
||||
return SampleUtils.deinterleave(chunk, getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
private class InterleavedComplexSamplesIterator implements Iterator<InterleavedComplexSamples>
|
||||
{
|
||||
private boolean mEmpty;
|
||||
|
||||
private int mBufferPointer = 0;
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
return !mEmpty;
|
||||
return mBufferPointer < mInterleavedComplexSamples.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InterleavedComplexSamples next()
|
||||
{
|
||||
if(mEmpty)
|
||||
if(!hasNext())
|
||||
{
|
||||
throw new IllegalStateException("No more samples");
|
||||
}
|
||||
|
||||
mEmpty = true;
|
||||
return new InterleavedComplexSamples(mInterleavedComplexSamples, getTimestamp());
|
||||
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
|
||||
mBufferPointer += BUFFER_SIZE * 2;
|
||||
|
||||
return new InterleavedComplexSamples(chunk, getTimestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -22,6 +22,7 @@ package io.github.dsheirer.channel.metadata;
|
||||
import io.github.dsheirer.alias.Alias;
|
||||
import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.alias.AliasModel;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateListener;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
@@ -35,11 +36,10 @@ import io.github.dsheirer.identifier.configuration.SystemConfigurationIdentifier
|
||||
import io.github.dsheirer.identifier.decoder.ChannelStateIdentifier;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Channel metadata containing details about the channel configuration, decoder state and current
|
||||
@@ -58,6 +58,7 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
private DecoderTypeConfigurationIdentifier mDecoderTypeConfigurationIdentifier;
|
||||
private Identifier mFromIdentifier;
|
||||
private List<Alias> mFromIdentifierAliases;
|
||||
private Identifier mTalkerAliasIdentifier;
|
||||
private Identifier mToIdentifier;
|
||||
private List<Alias> mToIdentifierAliases;
|
||||
private Integer mTimeslot;
|
||||
@@ -66,17 +67,51 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
private AliasModel mAliasModel;
|
||||
private AliasList mAliasList;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param aliasModel for alias lookups
|
||||
* @param timeslot for this metadata
|
||||
*/
|
||||
public ChannelMetadata(AliasModel aliasModel, Integer timeslot)
|
||||
{
|
||||
mAliasModel = aliasModel;
|
||||
mTimeslot = timeslot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param aliasModel for alias lookups
|
||||
*/
|
||||
public ChannelMetadata(AliasModel aliasModel)
|
||||
{
|
||||
this(aliasModel, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a textual description of this channel metadata.
|
||||
*/
|
||||
public String getDescription()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Channel Metadata Description\n");
|
||||
sb.append("\tTimeslot: ").append(mTimeslot).append("\n");
|
||||
Identifier decoder = getDecoderTypeConfigurationIdentifier();
|
||||
sb.append("\tDecoder: ").append(decoder != null ? decoder : "(null)").append("\n");
|
||||
Identifier state = getChannelStateIdentifier();
|
||||
sb.append("\tState: ").append(state != null ? state : "(null)").append("\n");
|
||||
Identifier system = getSystemConfigurationIdentifier();
|
||||
sb.append("\tSystem: ").append(system != null ? system : "(null)").append("\n");
|
||||
Identifier site = getSiteConfigurationIdentifier();
|
||||
sb.append("\tSite: ").append(site != null ? site : "(null)").append("\n");
|
||||
Identifier channel = getChannelNameConfigurationIdentifier();
|
||||
sb.append("\tChannel: ").append(channel != null ? channel : "(null)").append("\n");
|
||||
Identifier frequency = getFrequencyConfigurationIdentifier();
|
||||
sb.append("\tFrequency: ").append(frequency != null ? frequency : "(null)").append("\n");
|
||||
Identifier logical = getDecoderLogicalChannelNameIdentifier();
|
||||
sb.append("\tLogical Channel: ").append(logical != null ? logical : "(null)").append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Integer getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
@@ -205,6 +240,22 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
return mFromIdentifierAliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional talker alias identifier
|
||||
*/
|
||||
public Identifier getTalkerAliasIdentifier()
|
||||
{
|
||||
return mTalkerAliasIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if we have a non-null talker alias identifier
|
||||
*/
|
||||
public boolean hasTalkerAliasIdentifier()
|
||||
{
|
||||
return mTalkerAliasIdentifier != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current call event TO identifier
|
||||
*/
|
||||
@@ -376,15 +427,22 @@ public class ChannelMetadata implements Listener<IdentifierUpdateNotification>,
|
||||
case USER:
|
||||
if(identifier.getRole() == Role.FROM)
|
||||
{
|
||||
mFromIdentifier = update.isAdd() ? identifier : null;
|
||||
|
||||
if(mAliasList != null && mFromIdentifier != null)
|
||||
if(identifier.getForm() == Form.TALKER_ALIAS)
|
||||
{
|
||||
mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier);
|
||||
mTalkerAliasIdentifier = identifier;
|
||||
}
|
||||
else
|
||||
{
|
||||
mFromIdentifierAliases = Collections.EMPTY_LIST;
|
||||
mFromIdentifier = update.isAdd() ? identifier : null;
|
||||
|
||||
if(mAliasList != null && mFromIdentifier != null)
|
||||
{
|
||||
mFromIdentifierAliases = mAliasList.getAliases(mFromIdentifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
mFromIdentifierAliases = Collections.EMPTY_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
broadcastUpdate(ChannelMetadataField.USER_FROM);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.channel.metadata;
|
||||
|
||||
@@ -29,16 +26,16 @@ import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.decoder.DecoderLogicalChannelNameIdentifier;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.awt.EventQueue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
|
||||
public class ChannelMetadataModel extends AbstractTableModel implements IChannelMetadataUpdateListener
|
||||
{
|
||||
@@ -84,6 +81,47 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a diagnostic description of the channel metadata and metadata-channel map.
|
||||
* @return diagnostic description.
|
||||
*/
|
||||
public String getDiagnosticInformation()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Map<ChannelMetadata,Channel> mapCopy = new HashMap<>(mMetadataChannelMap);
|
||||
List<ChannelMetadata> metadataCopy = new ArrayList<>(mChannelMetadata);
|
||||
|
||||
sb.append("Channel Metadata Model Diagnostics\n");
|
||||
|
||||
for(ChannelMetadata channelMetadata: metadataCopy)
|
||||
{
|
||||
sb.append("\n--------------- CHANNEL METADATA ITEM --------------------\n\n");
|
||||
if(mapCopy.containsKey(channelMetadata))
|
||||
{
|
||||
Channel channel = mapCopy.get(channelMetadata);
|
||||
sb.append("\tMapped Channel:").append(channel).append("\n");
|
||||
sb.append("\t\tSource Configuration: ").append(channel.getSourceConfiguration()).append("\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("\tMapped Channel: **Channel Metadata Is Not In the Metadata:Channel Map**\n");
|
||||
}
|
||||
|
||||
sb.append(channelMetadata.getDescription());
|
||||
}
|
||||
|
||||
//Check for orphaned map entries that don't match the current channel metadata list.
|
||||
for(Map.Entry<ChannelMetadata,Channel> entry: mapCopy.entrySet())
|
||||
{
|
||||
if(!metadataCopy.contains(entry.getKey()))
|
||||
{
|
||||
sb.append("\n\tWARNING: orphaned Metadata Channel map entry - channel: " + entry.getValue()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
{
|
||||
MyEventBus.getGlobalEventBus().unregister(this);
|
||||
@@ -241,7 +279,8 @@ public class ChannelMetadataModel extends AbstractTableModel implements IChannel
|
||||
}
|
||||
else
|
||||
{
|
||||
String value = id.getValue().replace(" TS0", "").replace(" TS1", "");
|
||||
String value = id.getValue().replace(" TS1", "")
|
||||
.replace(" TS2", "");
|
||||
return value + " TS:" + channelMetadata.getTimeslot();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -38,6 +38,14 @@ import io.github.dsheirer.preference.identifier.TalkgroupFormatPreference;
|
||||
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -52,14 +60,6 @@ import javax.swing.SwingConstants;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ChannelMetadataPanel extends JPanel implements ListSelectionListener
|
||||
{
|
||||
@@ -294,6 +294,10 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
text = EMPTY_VALUE;
|
||||
}
|
||||
else if(hasAdditionalIdentifier(channelMetadata))
|
||||
{
|
||||
text = text + " " + getAdditionalIdentifier(channelMetadata);
|
||||
}
|
||||
|
||||
label.setText(text);
|
||||
}
|
||||
@@ -306,6 +310,8 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
}
|
||||
|
||||
public abstract Identifier getIdentifier(ChannelMetadata channelMetadata);
|
||||
public abstract boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata);
|
||||
public abstract Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,6 +329,18 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
return channelMetadata.getFromIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata)
|
||||
{
|
||||
return channelMetadata.getTalkerAliasIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata)
|
||||
{
|
||||
return channelMetadata.hasTalkerAliasIdentifier();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,6 +358,11 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
|
||||
{
|
||||
return channelMetadata.getToIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Identifier getAdditionalIdentifier(ChannelMetadata channelMetadata) {return null;}
|
||||
@Override
|
||||
public boolean hasAdditionalIdentifier(ChannelMetadata channelMetadata) {return false;}
|
||||
}
|
||||
|
||||
public class ColoredStateCellRenderer extends DefaultTableCellRenderer
|
||||
|
||||
@@ -33,52 +33,105 @@ import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
|
||||
/**
|
||||
* Swing panel for Now Playing channels table and channel details tab set.
|
||||
*/
|
||||
public class NowPlayingPanel extends JPanel
|
||||
{
|
||||
private ChannelMetadataPanel mChannelMetadataPanel;
|
||||
private ChannelDetailPanel mChannelDetailPanel;
|
||||
private DecodeEventPanel mDecodeEventPanel;
|
||||
private MessageActivityPanel mMessageActivityPanel;
|
||||
private ChannelPowerPanel mChannelPowerPanel;
|
||||
private final ChannelMetadataPanel mChannelMetadataPanel;
|
||||
private final ChannelDetailPanel mChannelDetailPanel;
|
||||
private final DecodeEventPanel mDecodeEventPanel;
|
||||
private final MessageActivityPanel mMessageActivityPanel;
|
||||
private final ChannelPowerPanel mChannelPowerPanel;
|
||||
private JideTabbedPane mTabbedPane;
|
||||
private JideSplitPane mSplitPane;
|
||||
private boolean mDetailTabsVisible;
|
||||
|
||||
/**
|
||||
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
|
||||
* messages, events, and spectral view.
|
||||
*/
|
||||
public NowPlayingPanel(PlaylistManager playlistManager, IconModel iconModel, UserPreferences userPreferences,
|
||||
SettingsManager settingsManager)
|
||||
SettingsManager settingsManager, boolean detailTabsVisible)
|
||||
{
|
||||
mChannelDetailPanel = new ChannelDetailPanel(playlistManager.getChannelProcessingManager());
|
||||
mDecodeEventPanel = new DecodeEventPanel(iconModel, userPreferences, playlistManager.getAliasModel());
|
||||
mMessageActivityPanel = new MessageActivityPanel(userPreferences);
|
||||
mChannelMetadataPanel = new ChannelMetadataPanel(playlistManager, iconModel, userPreferences);
|
||||
mChannelPowerPanel = new ChannelPowerPanel(playlistManager, settingsManager);
|
||||
mDetailTabsVisible = detailTabsVisible;
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the visibility of the channel details tabs panel.
|
||||
* @param visible true to show or false to hide.
|
||||
*/
|
||||
public void setDetailTabsVisible(boolean visible)
|
||||
{
|
||||
//Only adjust if there is a change in state
|
||||
if(visible ^ mDetailTabsVisible)
|
||||
{
|
||||
mDetailTabsVisible = visible;
|
||||
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
else
|
||||
{
|
||||
getSplitPane().remove(getTabbedPane());
|
||||
}
|
||||
|
||||
revalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private JideTabbedPane getTabbedPane()
|
||||
{
|
||||
if(mTabbedPane == null)
|
||||
{
|
||||
mTabbedPane = new JideTabbedPane();
|
||||
mTabbedPane.addTab("Details", mChannelDetailPanel);
|
||||
mTabbedPane.addTab("Events", mDecodeEventPanel);
|
||||
mTabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
mTabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||
mTabbedPane.setFont(this.getFont());
|
||||
mTabbedPane.setForeground(Color.BLACK);
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
mTabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(getTabbedPane().getSelectedIndex() == getTabbedPane()
|
||||
.indexOfComponent(mChannelPowerPanel)));
|
||||
}
|
||||
|
||||
return mTabbedPane;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split pane for channels table and channel details tabs.
|
||||
*/
|
||||
private JideSplitPane getSplitPane()
|
||||
{
|
||||
if(mSplitPane == null)
|
||||
{
|
||||
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
mSplitPane.setShowGripper(true);
|
||||
}
|
||||
|
||||
return mSplitPane;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout( new MigLayout( "insets 0 0 0 0", "[grow,fill]", "[grow,fill]") );
|
||||
getSplitPane().add(mChannelMetadataPanel);
|
||||
|
||||
JideTabbedPane tabbedPane = new JideTabbedPane();
|
||||
tabbedPane.addTab("Details", mChannelDetailPanel);
|
||||
tabbedPane.addTab("Events", mDecodeEventPanel);
|
||||
tabbedPane.addTab("Messages", mMessageActivityPanel);
|
||||
tabbedPane.addTab("Channel", mChannelPowerPanel);
|
||||
tabbedPane.setFont(this.getFont());
|
||||
tabbedPane.setForeground(Color.BLACK);
|
||||
|
||||
//Register state change listener to toggle visibility state for channel tab to turn-on/off FFT processing
|
||||
tabbedPane.addChangeListener(e -> mChannelPowerPanel.setPanelVisible(tabbedPane.getSelectedIndex() == tabbedPane
|
||||
.indexOfComponent(mChannelPowerPanel)));
|
||||
|
||||
JideSplitPane splitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
|
||||
splitPane.setShowGripper(true);
|
||||
splitPane.add(mChannelMetadataPanel);
|
||||
splitPane.add(tabbedPane);
|
||||
add(splitPane);
|
||||
if(mDetailTabsVisible)
|
||||
{
|
||||
getSplitPane().add(getTabbedPane());
|
||||
}
|
||||
|
||||
add(getSplitPane());
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mChannelDetailPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mDecodeEventPanel);
|
||||
mChannelMetadataPanel.addProcessingChainSelectionListener(mMessageActivityPanel);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -35,11 +35,10 @@ import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.heartbeat.Heartbeat;
|
||||
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AbstractChannelState extends Module implements IChannelEventProvider, IDecodeEventProvider,
|
||||
IDecoderStateEventProvider, ISourceEventProvider, IHeartbeatListener, ISquelchStateProvider,
|
||||
IdentifierUpdateProvider, IOverflowListener
|
||||
@@ -53,7 +52,8 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
|
||||
private Channel mChannel;
|
||||
protected boolean mSourceOverflow = false;
|
||||
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
|
||||
|
||||
protected boolean mTeardownSequenceStarted = false;
|
||||
protected boolean mTeardownSequenceCompleted = false;
|
||||
|
||||
//TODO: remove the IOverflowListener code from this class
|
||||
|
||||
@@ -66,6 +66,22 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
|
||||
mChannel = channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the teardown sequence was started.
|
||||
*/
|
||||
public boolean isTeardownSequenceCompleted()
|
||||
{
|
||||
return mTeardownSequenceCompleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the teardown sequence was completed, meaning that the request disable channel event was dispatched.
|
||||
*/
|
||||
public boolean isTeardownSequenceStarted()
|
||||
{
|
||||
return mTeardownSequenceStarted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates/replaces the current channel configuration with the argument.
|
||||
*/
|
||||
@@ -89,6 +105,11 @@ public abstract class AbstractChannelState extends Module implements IChannelEve
|
||||
*/
|
||||
protected abstract void checkState();
|
||||
|
||||
/**
|
||||
* Indicates if any timeslot is currently in a TEARDOWN state.
|
||||
*/
|
||||
public abstract boolean isTeardownState();
|
||||
|
||||
public abstract List<ChannelMetadata> getChannelMetadata();
|
||||
|
||||
public abstract void updateChannelStateIdentifiers(IdentifierUpdateNotification notification);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.channel.state;
|
||||
@@ -29,6 +26,7 @@ import io.github.dsheirer.message.IMessageListener;
|
||||
import io.github.dsheirer.module.Module;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.module.decode.event.ActivitySummaryProvider;
|
||||
import io.github.dsheirer.module.decode.event.DecodeEventDuplicateDetector;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEvent;
|
||||
import io.github.dsheirer.module.decode.event.IDecodeEventProvider;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
@@ -47,9 +45,38 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
protected Broadcaster<IDecodeEvent> mDecodeEventBroadcaster = new Broadcaster<>();
|
||||
protected Listener<DecoderStateEvent> mDecoderStateListener;
|
||||
private DecoderStateEventListener mDecoderStateEventListener = new DecoderStateEventListener();
|
||||
private static final DecodeEventDuplicateDetector mDuplicateEventDetector = new DecodeEventDuplicateDetector();
|
||||
private boolean mRunning;
|
||||
|
||||
public abstract DecoderType getDecoderType();
|
||||
|
||||
/**
|
||||
* Implements module start and sets the mRunning flag to true so that messages can be processed.
|
||||
*/
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
mRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the module stop and sets the mRunning flag to false to stop message processing
|
||||
*/
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
mRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if this module is running and can process/pass messages down to sub-class implementations.
|
||||
* @return true if running
|
||||
*/
|
||||
public boolean isRunning()
|
||||
{
|
||||
return mRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides subclass reference to the decode event broadcaster
|
||||
*/
|
||||
@@ -80,7 +107,10 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
*/
|
||||
protected void broadcast(IDecodeEvent event)
|
||||
{
|
||||
mDecodeEventBroadcaster.broadcast(event);
|
||||
if(!mDuplicateEventDetector.isDuplicate(event, System.currentTimeMillis()))
|
||||
{
|
||||
mDecodeEventBroadcaster.broadcast(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,6 +166,9 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
mDecoderStateListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper that implements the IDecoderStateEventListener interface
|
||||
*/
|
||||
private class DecoderStateEventListener implements Listener<DecoderStateEvent>
|
||||
{
|
||||
@Override
|
||||
@@ -144,5 +177,4 @@ public abstract class AbstractDecoderState extends Module implements ActivitySum
|
||||
receiveDecoderStateEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2020 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.channel.state;
|
||||
|
||||
import io.github.dsheirer.channel.state.DecoderStateEvent.Event;
|
||||
import io.github.dsheirer.identifier.Form;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.string.SimpleStringIdentifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Basic decoder channel state - provides the minimum channel state functionality
|
||||
* to support an always un-squelched audio decoder.
|
||||
*/
|
||||
public class AlwaysUnsquelchedDecoderState extends DecoderState
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(AlwaysUnsquelchedDecoderState.class);
|
||||
private static final String NO_SQUELCH = "No Squelch";
|
||||
private Identifier mChannelNameIdentifier;
|
||||
|
||||
private DecoderType mDecoderType;
|
||||
private String mChannelName;
|
||||
|
||||
public AlwaysUnsquelchedDecoderState(DecoderType decoderType, String channelName)
|
||||
{
|
||||
mDecoderType = decoderType;
|
||||
mChannelName = (channelName != null && !channelName.isEmpty()) ? channelName : decoderType.name() + " CHANNEL";
|
||||
mChannelNameIdentifier = new SimpleStringIdentifier(mChannelName, IdentifierClass.USER, Form.CHANNEL_NAME, Role.TO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init()
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getActivitySummary()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("Activity Summary\n");
|
||||
sb.append("\tDecoder:\t");
|
||||
sb.append(mDecoderType);
|
||||
sb.append("\n\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(IMessage t)
|
||||
{
|
||||
/* Not implemented */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveDecoderStateEvent(DecoderStateEvent event)
|
||||
{
|
||||
if(event.getEvent() == Event.REQUEST_RESET)
|
||||
{
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DecoderType getDecoderType()
|
||||
{
|
||||
return mDecoderType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
broadcast(new DecoderStateEvent(this, Event.REQUEST_ALWAYS_UNSQUELCH, State.IDLE));
|
||||
getIdentifierCollection().update(mChannelNameIdentifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -25,6 +25,7 @@ import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.DecoderTypeConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
@@ -38,13 +39,21 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
private MutableIdentifierCollection mIdentifierCollection;
|
||||
protected Listener<IdentifierUpdateNotification> mConfigurationIdentifierListener;
|
||||
protected IChannelDescriptor mCurrentChannel;
|
||||
private long mCurrentFrequency;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param mutableIdentifierCollection to preload into this decoder state
|
||||
*/
|
||||
public DecoderState(MutableIdentifierCollection mutableIdentifierCollection)
|
||||
{
|
||||
mIdentifierCollection = mutableIdentifierCollection;
|
||||
mIdentifierCollection.update(new DecoderTypeConfigurationIdentifier(getDecoderType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance using an empty identifier collection.
|
||||
*/
|
||||
public DecoderState()
|
||||
{
|
||||
this(new MutableIdentifierCollection());
|
||||
@@ -54,10 +63,28 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
super.start();
|
||||
//Broadcast the existing identifiers (as add events) so that they can be received by external listeners
|
||||
mIdentifierCollection.broadcastIdentifiers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Current frequency for this channel.
|
||||
*/
|
||||
public long getCurrentFrequency()
|
||||
{
|
||||
return mCurrentFrequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current frequency for this channel.
|
||||
* @param frequency to set.
|
||||
*/
|
||||
public void setCurrentFrequency(long frequency)
|
||||
{
|
||||
mCurrentFrequency = frequency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive identifier update notifications
|
||||
*/
|
||||
@@ -95,7 +122,7 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
/**
|
||||
* Sets the current channel descriptor
|
||||
*/
|
||||
protected void setCurrentChannel(IChannelDescriptor channel)
|
||||
public void setCurrentChannel(IChannelDescriptor channel)
|
||||
{
|
||||
mCurrentChannel = channel;
|
||||
}
|
||||
@@ -152,9 +179,13 @@ public abstract class DecoderState extends AbstractDecoderState
|
||||
{
|
||||
getIdentifierCollection().receive(identifierUpdateNotification);
|
||||
|
||||
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier)
|
||||
if(identifierUpdateNotification.getIdentifier() instanceof ChannelDescriptorConfigurationIdentifier cdci)
|
||||
{
|
||||
setCurrentChannel(((ChannelDescriptorConfigurationIdentifier)identifierUpdateNotification.getIdentifier()).getValue());
|
||||
setCurrentChannel(cdci.getValue());
|
||||
}
|
||||
else if(identifierUpdateNotification.getIdentifier() instanceof FrequencyConfigurationIdentifier fci)
|
||||
{
|
||||
setCurrentFrequency(fci.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -44,13 +44,12 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Multi-Channel state tracks the overall state of all processing modules and decoders configured for the channel
|
||||
@@ -166,6 +165,7 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
mStateMachineMap.get(timeslot).setState(State.IDLE);
|
||||
break;
|
||||
case TEARDOWN:
|
||||
mTeardownSequenceStarted = true;
|
||||
if(getChannel().isTrafficChannel())
|
||||
{
|
||||
checkTeardown();
|
||||
@@ -206,7 +206,16 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
{
|
||||
if(getChannel().isTrafficChannel())
|
||||
{
|
||||
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
|
||||
try
|
||||
{
|
||||
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error broadcasting shutdown channel event", t);
|
||||
}
|
||||
|
||||
mTeardownSequenceStarted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -238,6 +247,20 @@ public class MultiChannelState extends AbstractChannelState implements IDecoderS
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTeardownState()
|
||||
{
|
||||
for(StateMachine stateMachine: mStateMachineMap.values())
|
||||
{
|
||||
if(stateMachine.getState() == State.TEARDOWN)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates configuration identifiers for the channel name, system, site and alias list name.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -46,11 +46,10 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Channel state tracks the overall state of all processing modules and decoders configured for the channel and
|
||||
@@ -163,9 +162,11 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
mStateMachine.setState(State.IDLE);
|
||||
break;
|
||||
case TEARDOWN:
|
||||
mTeardownSequenceStarted = true;
|
||||
if(getChannel().isTrafficChannel())
|
||||
{
|
||||
broadcast(new ChannelEvent(getChannel(), ChannelEvent.Event.REQUEST_DISABLE));
|
||||
mTeardownSequenceCompleted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -175,6 +176,12 @@ public class SingleChannelState extends AbstractChannelState implements IDecoder
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTeardownState()
|
||||
{
|
||||
return mStateMachine.getState() == State.TEARDOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkState()
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -26,12 +26,20 @@ import io.github.dsheirer.identifier.IdentifierClass;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.MutableIdentifierCollection;
|
||||
import io.github.dsheirer.identifier.configuration.ChannelDescriptorConfigurationIdentifier;
|
||||
import io.github.dsheirer.identifier.configuration.FrequencyConfigurationIdentifier;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
|
||||
/**
|
||||
* Base decoder state implementation for a multi-timeslot protocol that tracks the state for a single timeslot.
|
||||
*/
|
||||
public abstract class TimeslotDecoderState extends DecoderState
|
||||
{
|
||||
private int mTimeslot;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param timeslot to monitor/maintain state.
|
||||
*/
|
||||
public TimeslotDecoderState(int timeslot)
|
||||
{
|
||||
super(new MutableIdentifierCollection(timeslot));
|
||||
@@ -39,6 +47,9 @@ public abstract class TimeslotDecoderState extends DecoderState
|
||||
mConfigurationIdentifierListener = new TimeslotConfigurationIdentifierListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitored timeslot for this decoder state instance.
|
||||
*/
|
||||
protected int getTimeslot()
|
||||
{
|
||||
return mTimeslot;
|
||||
@@ -82,6 +93,10 @@ public abstract class TimeslotDecoderState extends DecoderState
|
||||
{
|
||||
setCurrentChannel((IChannelDescriptor)identifier);
|
||||
}
|
||||
else if(identifier instanceof FrequencyConfigurationIdentifier fci)
|
||||
{
|
||||
setCurrentFrequency(fci.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,17 +59,25 @@ public class ControllerPanel extends JPanel
|
||||
|
||||
public ControllerPanel(PlaylistManager playlistManager, AudioPlaybackManager audioPlaybackManager,
|
||||
IconModel iconModel, MapService mapService, SettingsManager settingsManager,
|
||||
TunerManager tunerManager, UserPreferences userPreferences)
|
||||
TunerManager tunerManager, UserPreferences userPreferences, boolean detailTabsVisible)
|
||||
{
|
||||
mAudioPanel = new AudioPanel(iconModel, userPreferences, settingsManager, audioPlaybackManager,
|
||||
playlistManager.getAliasModel());
|
||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager);
|
||||
mNowPlayingPanel = new NowPlayingPanel(playlistManager, iconModel, userPreferences, settingsManager, detailTabsVisible);
|
||||
mMapPanel = new MapPanel(mapService, playlistManager.getAliasModel(), iconModel, settingsManager);
|
||||
mTunerManagerPanel = new TunerViewPanel(tunerManager, userPreferences);
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Now playing panel.
|
||||
*/
|
||||
public NowPlayingPanel getNowPlayingPanel()
|
||||
{
|
||||
return mNowPlayingPanel;
|
||||
}
|
||||
|
||||
private void init()
|
||||
{
|
||||
setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[]0[grow,fill]0[]"));
|
||||
@@ -97,8 +105,8 @@ public class ControllerPanel extends JPanel
|
||||
mTabbedPane.addTab("Map", mMapPanel);
|
||||
mTabbedPane.addTab("Tuners", mTunerManagerPanel);
|
||||
|
||||
Icon gearIcon = IconFontSwing.buildIcon(FontAwesome.COG, 20, Color.DARK_GRAY);
|
||||
mTabbedPane.addTab("Playlist Editor", gearIcon, new JLabel("Show Playlist Manager"));
|
||||
Icon playIcon = IconFontSwing.buildIcon(FontAwesome.PLAY_CIRCLE_O, 20, Color.DARK_GRAY);
|
||||
mTabbedPane.addTab("Playlist Editor", playIcon, new JLabel("Show Playlist Manager"));
|
||||
mSettingsTabIndex = mTabbedPane.getTabCount() - 1;
|
||||
|
||||
//Set preferred size to influence the split between these panels
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.controller.channel;
|
||||
|
||||
@@ -35,13 +32,16 @@ import io.github.dsheirer.record.RecorderType;
|
||||
import io.github.dsheirer.record.config.RecordConfiguration;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.SourceType;
|
||||
import io.github.dsheirer.source.config.SourceConfigFactory;
|
||||
import io.github.dsheirer.source.config.SourceConfigRecording;
|
||||
import io.github.dsheirer.source.config.SourceConfigTuner;
|
||||
import io.github.dsheirer.source.config.SourceConfigTunerMultipleFrequency;
|
||||
import io.github.dsheirer.source.config.SourceConfiguration;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import java.beans.Transient;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -55,9 +55,6 @@ import javafx.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.beans.Transient;
|
||||
import java.util.Objects;
|
||||
|
||||
@JacksonXmlRootElement(localName = "channel")
|
||||
public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
{
|
||||
@@ -88,7 +85,7 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
private BooleanProperty mAutoStart = new SimpleBooleanProperty();
|
||||
private IntegerProperty mAutoStartOrder = new SimpleIntegerProperty();
|
||||
private boolean mSelected;
|
||||
private TunerChannel mTunerChannel = null;
|
||||
private List<TunerChannel> mTunerChannels = null;
|
||||
|
||||
private ChannelType mChannelType = ChannelType.STANDARD;
|
||||
|
||||
@@ -634,9 +631,8 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
|
||||
updateFrequencies();
|
||||
|
||||
//Clear the tune channel object so that it can be recreated if the
|
||||
//source configuration changes
|
||||
mTunerChannel = null;
|
||||
//Clear the tune channels object so that it can be recreated if the source configuration changes
|
||||
mTunerChannels = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,35 +676,50 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method to construct a tuner channel object representing a
|
||||
* tuner or recording source frequency and bandwidth that can be used by
|
||||
* application components for graphically representing this channel.
|
||||
* Convenience method to construct a tuner channel object representing a tuner or recording source frequency and
|
||||
* bandwidth that can be used by application components for graphically representing this channel.
|
||||
*
|
||||
* If the source configuration is not a tuner or recording, this method
|
||||
* returns null.
|
||||
* If the source configuration is not a tuner or recording, this method returns an empty list.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public TunerChannel getTunerChannel()
|
||||
public List<TunerChannel> getTunerChannels()
|
||||
{
|
||||
if(mTunerChannel == null)
|
||||
if(mTunerChannels == null && mSourceConfiguration != null)
|
||||
{
|
||||
if(mSourceConfiguration.getSourceType() == SourceType.TUNER)
|
||||
{
|
||||
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
|
||||
mTunerChannels = new ArrayList<>();
|
||||
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth());
|
||||
}
|
||||
else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
|
||||
switch(mSourceConfiguration.getSourceType())
|
||||
{
|
||||
SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration;
|
||||
|
||||
mTunerChannel = new TunerChannel(config.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth());
|
||||
case TUNER:
|
||||
if(mSourceConfiguration instanceof SourceConfigTuner tunerConfig)
|
||||
{
|
||||
mTunerChannels.add(new TunerChannel(tunerConfig.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
|
||||
}
|
||||
break;
|
||||
case TUNER_MULTIPLE_FREQUENCIES:
|
||||
if(mSourceConfiguration instanceof SourceConfigTunerMultipleFrequency multiConfig)
|
||||
{
|
||||
for(long frequency: multiConfig.getFrequencies())
|
||||
{
|
||||
mTunerChannels.add(new TunerChannel(frequency,
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RECORDING:
|
||||
if(mSourceConfiguration instanceof SourceConfigRecording recordingConfig)
|
||||
{
|
||||
mTunerChannels.add(new TunerChannel(recordingConfig.getFrequency(),
|
||||
mDecodeConfiguration.getChannelSpecification().getBandwidth()));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.warn("Unrecognized channel source type: " + mSourceConfiguration.getSourceType());
|
||||
}
|
||||
}
|
||||
|
||||
return mTunerChannel;
|
||||
return mTunerChannels;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,9 +728,15 @@ public class Channel extends Configuration implements Listener<SourceEvent>
|
||||
*/
|
||||
public boolean isWithin(long minimum, long maximum)
|
||||
{
|
||||
TunerChannel tunerChannel = getTunerChannel();
|
||||
for(TunerChannel tunerChannel: getTunerChannels())
|
||||
{
|
||||
if(tunerChannel.overlaps(minimum, maximum))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return tunerChannel != null && tunerChannel.overlaps(minimum, maximum);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.controller.channel;
|
||||
|
||||
@@ -26,15 +23,13 @@ import io.github.dsheirer.controller.channel.Channel.ChannelType;
|
||||
import io.github.dsheirer.controller.channel.ChannelEvent.Event;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Channel Model
|
||||
*/
|
||||
@@ -55,6 +50,58 @@ public class ChannelModel implements Listener<ChannelEvent>
|
||||
mTrafficChannels.addListener(changeListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a deduplicated list of alias list names from across the channel configurations.
|
||||
* @return list of alias list names.
|
||||
*/
|
||||
public List<String> getAliasListNames()
|
||||
{
|
||||
List<String> aliasListNames = new ArrayList<>();
|
||||
|
||||
for(Channel channel : mChannels)
|
||||
{
|
||||
String aliasListName = channel.getAliasListName();
|
||||
|
||||
if(aliasListName != null && !aliasListName.isEmpty() && !aliasListNames.contains(aliasListName))
|
||||
{
|
||||
aliasListNames.add(aliasListName);
|
||||
}
|
||||
}
|
||||
|
||||
return aliasListNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the alias list across the set of aliases.
|
||||
* @param oldName currently used by the alias
|
||||
* @param newName to apply to the alias
|
||||
*/
|
||||
public void renameAliasList(String oldName, String newName)
|
||||
{
|
||||
if(oldName == null || oldName.isEmpty() || newName == null || newName.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mChannels.stream().filter(channel -> channel.getAliasListName().equals(oldName))
|
||||
.forEach(channel -> channel.setAliasListName(newName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes any aliases that have the alias list name
|
||||
* @param aliasListName to delete
|
||||
*/
|
||||
public void deleteAliasList(String aliasListName)
|
||||
{
|
||||
if(aliasListName == null || aliasListName.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mChannels.stream().filter(channel -> channel.getAliasListName().equals(aliasListName))
|
||||
.forEach(channel -> channel.setAliasListName(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable list of channel configurations managed by this model
|
||||
*/
|
||||
@@ -109,6 +156,45 @@ public class ChannelModel implements Listener<ChannelEvent>
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames the alias list across all channels.
|
||||
* @param existing alias list name
|
||||
* @param renamed alias list name.
|
||||
*/
|
||||
public void renameAliaslistForChannels(String existing, String renamed)
|
||||
{
|
||||
if(existing == null || existing.isEmpty() || renamed == null || renamed.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mChannels.forEach(channel -> {
|
||||
if(channel.getAliasListName() != null && channel.getAliasListName().equals(existing))
|
||||
{
|
||||
channel.setAliasListName(renamed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the alias list name from any channels.
|
||||
* @param name to delete
|
||||
*/
|
||||
public void deleteAliasListName(String name)
|
||||
{
|
||||
if(name == null || name.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mChannels.forEach(channel -> {
|
||||
if(channel.getAliasListName() != null && channel.getAliasListName().equals(name))
|
||||
{
|
||||
channel.setAliasListName(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of unique system values from across the channel set
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.AudioSegment;
|
||||
import io.github.dsheirer.channel.metadata.ChannelAndMetadata;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadata;
|
||||
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
|
||||
import io.github.dsheirer.channel.state.AbstractChannelState;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStartProcessingRequest;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||
import io.github.dsheirer.controller.channel.event.PreloadDataContent;
|
||||
@@ -52,6 +53,7 @@ import io.github.dsheirer.source.tuner.manager.TunerManager;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.awt.GraphicsEnvironment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -72,9 +74,10 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
{
|
||||
private static final String DIVIDER = "-------------------------------------------------------------------------\n";
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ChannelProcessingManager.class);
|
||||
private static final String TUNER_UNAVAILABLE_DESCRIPTION = "TUNER UNAVAILABLE";
|
||||
private Map<Channel,ProcessingChain> mProcessingChains = new ConcurrentHashMap<>();
|
||||
private Map<Channel,ProcessingChain> mProcessingChainsMap = new ConcurrentHashMap<>();
|
||||
private Lock mLock = new ReentrantLock();
|
||||
|
||||
private ChannelSourceEventErrorListener mSourceErrorListener = new ChannelSourceEventErrorListener();
|
||||
@@ -131,7 +134,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
isProcessing = mProcessingChains.containsKey(channel) && mProcessingChains.get(channel).isProcessing();
|
||||
isProcessing = mProcessingChainsMap.containsKey(channel) && mProcessingChainsMap.get(channel).isProcessing();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -147,7 +150,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
*/
|
||||
public boolean isProcessing()
|
||||
{
|
||||
return !mProcessingChains.isEmpty();
|
||||
return !mProcessingChainsMap.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +159,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
*/
|
||||
public ProcessingChain getProcessingChain(Channel channel)
|
||||
{
|
||||
return mProcessingChains.get(channel);
|
||||
return mProcessingChainsMap.get(channel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,7 +178,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChainsMap.entrySet())
|
||||
{
|
||||
if(entry.getValue() == processingChain)
|
||||
{
|
||||
@@ -206,7 +209,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChains.entrySet())
|
||||
for(Map.Entry<Channel,ProcessingChain> entry : mProcessingChainsMap.entrySet())
|
||||
{
|
||||
if(entry.getValue().hasSource(tunerChannelSource))
|
||||
{
|
||||
@@ -276,15 +279,27 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
break;
|
||||
case REQUEST_DISABLE:
|
||||
case NOTIFICATION_DELETE:
|
||||
if(channel.isProcessing())
|
||||
if(channel != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
stopProcessing(channel);
|
||||
}
|
||||
catch(ChannelException ce)
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error stopping channel [" + channel.getName() + "] - " + ce.getMessage());
|
||||
mLog.error("Error stopping channel [" + channel + "]", t);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
throw new IllegalArgumentException("Request to disable channel must have non-null channel");
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.error("Caught a [" + event.getEvent() + "] non-standard channel event - logging stack trace. " +
|
||||
"This should not happen, please send this error to the developer.", iae);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -365,7 +380,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
* @param request containing channel and other details
|
||||
* @throws ChannelException if a source is not available for the channel
|
||||
*/
|
||||
private void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
|
||||
private synchronized void startProcessing(ChannelStartProcessingRequest request) throws ChannelException
|
||||
{
|
||||
Channel channel = request.getChannel();
|
||||
|
||||
@@ -379,8 +394,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
String threadName = "sdrtrunk channel [" + channel.getChannelID() + "/" +
|
||||
channel.getDecodeConfiguration().getDecoderType().getShortDisplayString() + "]";
|
||||
source = mTunerManager.getSource(channel.getSourceConfiguration(),
|
||||
channel.getDecodeConfiguration().getChannelSpecification());
|
||||
channel.getDecodeConfiguration().getChannelSpecification(), threadName);
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
@@ -390,7 +407,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
if(source == null)
|
||||
{
|
||||
//This has to be done on the FX event thread when the playlist editor is constructed
|
||||
Platform.runLater(() -> channel.setProcessing(false));
|
||||
if(!GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
Platform.runLater(() -> channel.setProcessing(false));
|
||||
}
|
||||
|
||||
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel,
|
||||
ChannelEvent.Event.NOTIFICATION_PROCESSING_START_REJECTED, TUNER_UNAVAILABLE_DESCRIPTION));
|
||||
@@ -442,7 +462,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
/* Processing Modules */
|
||||
List<Module> modules = DecoderFactory.getModules(mChannelMapModel, channel, mAliasModel, mUserPreferences,
|
||||
request.getTrafficChannelManager());
|
||||
request.getTrafficChannelManager(), request.getChannelDescriptor());
|
||||
processingChain.addModules(modules);
|
||||
|
||||
//Post preload data from the request to the event bus. Modules that can handle preload data will annotate
|
||||
@@ -513,7 +533,14 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
if(addProcessingChain(channel, processingChain))
|
||||
{
|
||||
processingChain.start();
|
||||
try
|
||||
{
|
||||
processingChain.start();
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error caught during processing chain startup - continuing", t);
|
||||
}
|
||||
|
||||
if(GraphicsEnvironment.isHeadless())
|
||||
{
|
||||
@@ -556,10 +583,10 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
if(!mProcessingChains.containsKey(channel))
|
||||
if(!mProcessingChainsMap.containsKey(channel))
|
||||
{
|
||||
added = true;
|
||||
mProcessingChains.put(channel, processingChain);
|
||||
mProcessingChainsMap.put(channel, processingChain);
|
||||
getChannelMetadataModel().add(new ChannelAndMetadata(channel, processingChain.getChannelState().getChannelMetadata()));
|
||||
}
|
||||
}
|
||||
@@ -584,7 +611,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
removed = mProcessingChains.remove(channel);
|
||||
removed = mProcessingChainsMap.remove(channel);
|
||||
|
||||
if(removed != null)
|
||||
{
|
||||
@@ -619,8 +646,28 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
}
|
||||
else
|
||||
{
|
||||
//This has to be done on the FX event thread when the playlist editor is constructed
|
||||
Platform.runLater(() -> channel.setProcessing(false));
|
||||
//When we're in non-headless mode we have to change the processing property on the JavaFX
|
||||
//event thread. However, if it hasn't yet been initialized (ie an FX window opened), we'll
|
||||
//get an ISE. In that case, just set the property to false because there won't be any
|
||||
//property listeners being triggered.
|
||||
try
|
||||
{
|
||||
Platform.runLater(() -> {
|
||||
try
|
||||
{
|
||||
channel.setProcessing(false);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Error during channel stop while setting processing to false [" + channel +
|
||||
"] - continuing channel stop process", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(IllegalStateException e)
|
||||
{
|
||||
channel.setProcessing(false);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
@@ -633,6 +680,8 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
processingChain.removeFrequencyChangeListener(channel);
|
||||
channel.resetFrequencyCorrection();
|
||||
|
||||
//Notify all processing chains that this channel is shutting down so that if this is a traffic channel,
|
||||
//the owning parent channel's traffic channel manager can cleanup it's accounting.
|
||||
mChannelEventBroadcaster.broadcast(new ChannelEvent(channel, ChannelEvent.Event.NOTIFICATION_PROCESSING_STOP));
|
||||
mChannelEventBroadcaster.removeListener(processingChain);
|
||||
|
||||
@@ -660,7 +709,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
mDelayedChannelStartTasks.remove(delayedTask);
|
||||
}
|
||||
|
||||
List<Channel> channelsToStop = new ArrayList<>(mProcessingChains.keySet());
|
||||
List<Channel> channelsToStop = new ArrayList<>(mProcessingChainsMap.keySet());
|
||||
|
||||
for(Channel channel : channelsToStop)
|
||||
{
|
||||
@@ -683,7 +732,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
public void convertToTrafficChannel(ChannelConversionRequest request)
|
||||
{
|
||||
//Update the channel to processing chain map.
|
||||
ProcessingChain processingChain = mProcessingChains.remove(request.getCurrentChannel());
|
||||
ProcessingChain processingChain = mProcessingChainsMap.remove(request.getCurrentChannel());
|
||||
|
||||
if(processingChain != null)
|
||||
{
|
||||
@@ -697,7 +746,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
request.getTrafficChannel().setProcessing(true);
|
||||
});
|
||||
|
||||
mProcessingChains.put(request.getTrafficChannel(), processingChain);
|
||||
mProcessingChainsMap.put(request.getTrafficChannel(), processingChain);
|
||||
mChannelMetadataModel.updateChannelMetadataToChannelMap(processingChain.getChannelState().getChannelMetadata(),
|
||||
request.getTrafficChannel());
|
||||
|
||||
@@ -803,7 +852,7 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
|
||||
try
|
||||
{
|
||||
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChains.entrySet())
|
||||
for(Map.Entry<Channel,ProcessingChain> entry: mProcessingChainsMap.entrySet())
|
||||
{
|
||||
if(entry.getValue().hasSource(sourceEvent.getSource()))
|
||||
{
|
||||
@@ -837,8 +886,64 @@ public class ChannelProcessingManager implements Listener<ChannelEvent>
|
||||
mLog.error("Error stopping channel [" + (toShutdown != null ? toShutdown.getName() : "unknown") +
|
||||
"] with source error - " + ce.getMessage());
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
mLog.error("Error stopping channel [" + toShutdown + "]", t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a diagnostic report.
|
||||
* @return report text.
|
||||
*/
|
||||
public String getDiagnosticInformation()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Channel Processing Manager - Diagnostics Report\n\n");
|
||||
sb.append(DIVIDER);
|
||||
sb.append("\tChannel to Processing Chain Map Contents\n");
|
||||
Map<Channel,ProcessingChain> mapCopy = new HashMap<>(mProcessingChainsMap);
|
||||
for(Map.Entry<Channel,ProcessingChain> entry: mapCopy.entrySet())
|
||||
{
|
||||
sb.append("\n\n--------------- CHANNEL:PROCESSING CHAIN MAP ENTRY --------------------\n");
|
||||
try
|
||||
{
|
||||
Channel channel = entry.getKey();
|
||||
sb.append("\tChannel: " + channel).append("\n");
|
||||
sb.append("\t\tSource Configuration: " + channel.getSourceConfiguration()).append("\n");
|
||||
ProcessingChain chain = entry.getValue();
|
||||
sb.append("\tProcessing Chain - Processing: ").append(chain.isProcessing()).append("\n");
|
||||
AbstractChannelState state = chain.getChannelState();
|
||||
sb.append("Channel State: ").append(state.getClass()).append("\n");
|
||||
sb.append(" Teardown Started:").append(state.isTeardownSequenceStarted()).append("\n");
|
||||
sb.append(" Teardown Completed:").append(state.isTeardownSequenceCompleted()).append("\n");
|
||||
for(ChannelMetadata metadata: state.getChannelMetadata())
|
||||
{
|
||||
sb.append(metadata.getDescription()).append("\n");
|
||||
}
|
||||
|
||||
Source source = chain.getSource();
|
||||
|
||||
if(source != null)
|
||||
{
|
||||
sb.append("Channel Source Class: " + source.getClass()).append("\n");
|
||||
sb.append("\t\tTo String:").append(source).append("\n");
|
||||
sb.append("\t\tHash:").append(Integer.toHexString(source.hashCode()).toUpperCase()).append("\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.append("Channel Source: (null)\n");
|
||||
}
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
sb.append("\tError while logging diagnostics of map entry - " + t.getMessage()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,8 +68,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
*/
|
||||
private static final int PROCESSED_CHANNEL_RESULTS_THRESHOLD = 1024;
|
||||
|
||||
//Sized to process 1/20th batch of 152 buffers a second, at 40 times per second
|
||||
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(152 / 20, 25);
|
||||
//Sized to process 40 times per second
|
||||
private IFFTProcessorDispatcher mIFFTProcessorDispatcher = new IFFTProcessorDispatcher(25);
|
||||
private FloatFFT_1D mFFT;
|
||||
private float[] mInlineSamples;
|
||||
private float[] mInlineFilter;
|
||||
@@ -406,9 +406,9 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
*/
|
||||
public class IFFTProcessorDispatcher extends Dispatcher<List<float[]>>
|
||||
{
|
||||
public IFFTProcessorDispatcher(int batchSize, long interval)
|
||||
public IFFTProcessorDispatcher(long interval)
|
||||
{
|
||||
super("sdrtrunk polyphase ifft processor", batchSize, interval);
|
||||
super("sdrtrunk polyphase ifft processor", interval);
|
||||
|
||||
//We create a listener interface to receive the batched channel results arrays from the scheduled thread pool
|
||||
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -23,6 +23,7 @@ import io.github.dsheirer.buffer.INativeBufferProvider;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.log.LoggingSuppressor;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
@@ -65,6 +66,7 @@ import org.slf4j.LoggerFactory;
|
||||
public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
{
|
||||
private static final DecimalFormat FREQUENCY_FORMAT = new DecimalFormat("0.00000");
|
||||
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(LoggerFactory.getLogger(PolyphaseChannelManager.class));
|
||||
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
|
||||
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
|
||||
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
||||
@@ -108,7 +110,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
}
|
||||
|
||||
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
|
||||
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 50, 10);
|
||||
mBufferDispatcher = new Dispatcher("sdrtrunk polyphase buffer processor", 10);
|
||||
mBufferDispatcher.setListener(mNativeBufferReceiver);
|
||||
}
|
||||
|
||||
@@ -145,6 +147,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
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);
|
||||
sb.append(" HASH:").append(Integer.toHexString(pcs.hashCode()).toUpperCase());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
@@ -186,9 +189,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
* Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be
|
||||
* sourced due to the current center frequency and/or sample rate.
|
||||
* @param tunerChannel specifying center frequency and bandwidth.
|
||||
* @param threadName for the channel's dispatcher
|
||||
* @return source or null.
|
||||
*/
|
||||
public TunerChannelSource getChannel(TunerChannel tunerChannel)
|
||||
public TunerChannelSource getChannel(TunerChannel tunerChannel, String threadName)
|
||||
{
|
||||
PolyphaseChannelSource channelSource = null;
|
||||
|
||||
@@ -197,13 +201,14 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
try
|
||||
{
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
|
||||
mChannelSourceEventListener);
|
||||
mChannelSourceEventListener, threadName);
|
||||
|
||||
mChannelSources.add(channelSource);
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
|
||||
LOGGING_SUPPRESSOR.error(iae.getMessage(), 3, "Couldn't allocate channel. " + iae.getMessage());
|
||||
channelSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,11 +252,16 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
synchronized(mBufferDispatcher)
|
||||
{
|
||||
mChannelSources.remove(channelSource);
|
||||
mPolyphaseChannelizer.removeChannel(channelSource);
|
||||
|
||||
if(mPolyphaseChannelizer != null)
|
||||
{
|
||||
mPolyphaseChannelizer.removeChannel(channelSource);
|
||||
}
|
||||
|
||||
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
|
||||
|
||||
//If this is the last/only channel, deregister to stop the sample buffers
|
||||
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
||||
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
|
||||
{
|
||||
mNativeBufferProvider.removeBufferListener(mBufferDispatcher);
|
||||
mBufferDispatcher.stop();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -56,12 +56,14 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
* @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 threadName for the channel's dispatcher
|
||||
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
|
||||
*/
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
|
||||
Listener<SourceEvent> producerSourceEventListener) throws IllegalArgumentException
|
||||
Listener<SourceEvent> producerSourceEventListener, String threadName)
|
||||
throws IllegalArgumentException
|
||||
{
|
||||
super(producerSourceEventListener, tunerChannel);
|
||||
super(producerSourceEventListener, tunerChannel, threadName);
|
||||
mChannelSampleRate = channelCalculator.getChannelSampleRate();
|
||||
doUpdateOutputProcessor(channelCalculator, filterManager);
|
||||
}
|
||||
@@ -222,7 +224,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
{
|
||||
case 1:
|
||||
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
|
||||
indexes, channelCalculator.getChannelCount(), getHeartbeatManager());
|
||||
indexes, channelCalculator.getChannelCount(), getHeartbeatManager(), mThreadName);
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
mPolyphaseChannelOutputProcessor.start();
|
||||
@@ -233,7 +235,7 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
|
||||
channelCalculator.getChannelBandwidth(), 2);
|
||||
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
|
||||
indexes, filter, channelCalculator.getChannelCount(), getHeartbeatManager());
|
||||
indexes, filter, channelCalculator.getChannelCount(), getHeartbeatManager(), mThreadName);
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
mPolyphaseChannelOutputProcessor.start();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -41,17 +41,15 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
||||
* oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer.
|
||||
*
|
||||
* @param inputChannelCount is the number of input channels for this output processor
|
||||
* @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
|
||||
* channel sample rate for frequency translation/correction.
|
||||
* @param heartbeatManager to receive pings on the dispatcher thread
|
||||
* @param threadName for the dispatcher
|
||||
*/
|
||||
public ChannelOutputProcessor(int inputChannelCount, double sampleRate, HeartbeatManager heartbeatManager)
|
||||
public ChannelOutputProcessor(int inputChannelCount, HeartbeatManager heartbeatManager, String threadName)
|
||||
{
|
||||
mInputChannelCount = inputChannelCount;
|
||||
//Process 1/10th of the sample rate per second at a rate of 20 times a second (200% of anticipated rate)
|
||||
mHeartbeatManager = heartbeatManager;
|
||||
mChannelResultsDispatcher = new Dispatcher("sdrtrunk polyphase channel", (int)(sampleRate / 10),
|
||||
50, mHeartbeatManager);
|
||||
mChannelResultsDispatcher = new Dispatcher(threadName,50, mHeartbeatManager);
|
||||
mChannelResultsDispatcher.setListener(floats -> {
|
||||
try
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -38,11 +38,12 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
* @param channelIndexes containing a single channel index.
|
||||
* @param gain value to apply. This is typically the same as the channelizer's channel count.
|
||||
* @param heartbeatManager to receive heartbeats on the dispatch thread
|
||||
* @param threadName to use for this processor
|
||||
*/
|
||||
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float gain,
|
||||
HeartbeatManager heartbeatManager)
|
||||
HeartbeatManager heartbeatManager, String threadName)
|
||||
{
|
||||
super(1, sampleRate, heartbeatManager);
|
||||
super(1, heartbeatManager, threadName);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
mMixerAssembler = new OneChannelMixerAssembler(gain);
|
||||
mMixerAssembler.getMixer().setSampleRate(sampleRate);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -37,15 +37,17 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
*
|
||||
* @param sampleRate of the output sample stream.
|
||||
* @param channelIndexes containing two channel indices.
|
||||
* @param filter to use
|
||||
* @param gain to apply to output. Typically this is equal to the channelizer's channel count.
|
||||
* @param heartbeatManager to be pinged on the dispatcher thread
|
||||
* @param threadName for the dispatcher
|
||||
*/
|
||||
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter, float gain,
|
||||
HeartbeatManager heartbeatManager)
|
||||
HeartbeatManager heartbeatManager, String threadName)
|
||||
{
|
||||
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
|
||||
//after synthesizing both input channels
|
||||
super(2, sampleRate, heartbeatManager);
|
||||
super(2, heartbeatManager, threadName);
|
||||
setPolyphaseChannelIndices(channelIndexes);
|
||||
mMixerAssembler = new TwoChannelMixerAssembler(gain);
|
||||
mMixerAssembler.getMixer().setSampleRate(sampleRate);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -16,61 +16,64 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.fsk;
|
||||
|
||||
import io.github.dsheirer.bits.MessageFramer;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.dc.IIRSinglePoleDCRemovalFilter;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
|
||||
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
|
||||
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
|
||||
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.Arrays;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Logic Trunked Radio (LTR) 300-baud FSK decoder is designed to operate against an 8 kHz FM demodulated audio stream.
|
||||
* LTR decoder designed to work with 8 kHz input audio samples and decode 300-baud signalling.
|
||||
*
|
||||
* This decoder employs a zero-crossing timing error detector with adjustable symbol timing error gain. This decoder
|
||||
* also employs a synchronization monitor that automatically adjusts symbol timing error gain according to the quantity
|
||||
* of successful message sync pattern detections. This is optimized for LTR signals that periodically transmit idle
|
||||
* bursts and transmit continuous message streams during voice transmissions. The synchronization monitor adjusts to an
|
||||
* aggressive gain value when sync is lost and automatically reduces timing error gain as successive sync pattern
|
||||
* detections occur.
|
||||
* This decoder monitors the incoming sample stream looking for either a positive or negative sequence of samples where
|
||||
* the energy delta across the baud period exceeds a threshold value and causes the symbol state to flip. The symbol
|
||||
* state remains across successive baud periods until the next state change sequence is detected.
|
||||
*
|
||||
* FM demodulated sample buffers should not be filtered prior to this decoder. An internal DC-removal filter coupled
|
||||
* with a low-pass filter removes any tuning offset and/or audio components above 300 Hertz. The DC removal filter is
|
||||
* adjusted for a slower offset removal in order to prevent overly distorting the FSK signalling component. This mild
|
||||
* adjustment may not fully compensate for mis-tuned signals and may result in missed decodes of idle bursts. However,
|
||||
* the DC filter will track a mis-tuned signal to baseband quickly upon a continuous transmission.
|
||||
* Valid LTR messages start with a zero symbol and transition to a positive symbol for the sync bit, this decoder
|
||||
* monitors for excessively long sequences of a one symbol and forces the symbol state back to a zero.
|
||||
*/
|
||||
public class LTRDecoder implements Listener<float[]>, ISyncDetectListener, ISyncStateListener
|
||||
public class LTRDecoder implements Listener<float[]>
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(LTRDecoder.class);
|
||||
|
||||
public static final float SAMPLES_PER_SYMBOL = 8000.0f / 300.0f;
|
||||
|
||||
protected static final float COARSE_TIMING_GAIN = 1.0f / 3.0f;
|
||||
protected static final float MEDIUM_TIMING_GAIN = 1.0f / 4.0f;
|
||||
protected static final float FINE_TIMING_GAIN = 1.0f / 5.0f;
|
||||
private static final Logger mLog = LoggerFactory.getLogger(LTRDecoder.class);
|
||||
//Slope calculation sumXX value is always the same for 27 evenly spaced samples
|
||||
private static final double SLOPE_CALCULATION_SUM_XX = 1637.999992057681;
|
||||
private static final float BAUD_LENGTH = 8000.0f / 300.0f;
|
||||
private static final float SLOPE_THRESHOLD = 0.0048f;
|
||||
private static final int MAX_ONES_SEQUENCE = 18;
|
||||
private static final int OVERLAP = 26;
|
||||
private static final int POST_TRANSITION_SAMPLES_TO_SKIP = 26;
|
||||
private static final int SLOPE_CALCULATION_LENGTH = 27;
|
||||
private static final int SYMBOL_TRANSITION_IDEAL_MIN = 11;
|
||||
private static final int SYMBOL_TRANSITION_IDEAL_MAX = 16;
|
||||
|
||||
private static float[] sLowPassFilterCoefficients;
|
||||
private boolean mSymbol = false;
|
||||
private float[] mResidual = new float[OVERLAP];
|
||||
private float mBaudCounter = 0f;
|
||||
private float mMaxSlope = 0;
|
||||
private int mExcessiveOneSequenceCounter = 0;
|
||||
private Listener<boolean[]> mSymbolListener;
|
||||
|
||||
static
|
||||
{
|
||||
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
|
||||
.sampleRate(8000)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(500)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.03) //Approximately 60 dB attenuation
|
||||
.build();
|
||||
.sampleRate(8000)
|
||||
.gridDensity(16)
|
||||
.oddLength(true)
|
||||
.passBandCutoff(300)
|
||||
.passBandAmplitude(1.0)
|
||||
.passBandRipple(0.01)
|
||||
.stopBandStart(500)
|
||||
.stopBandAmplitude(0.0)
|
||||
.stopBandRipple(0.03) //Approximately 60 dB attenuation
|
||||
.build();
|
||||
|
||||
try
|
||||
{
|
||||
@@ -87,143 +90,193 @@ public class LTRDecoder implements Listener<float[]>, ISyncDetectListener, ISync
|
||||
}
|
||||
}
|
||||
|
||||
protected float mSymbolTimingGain = COARSE_TIMING_GAIN;
|
||||
protected SampleBuffer mSampleBuffer;
|
||||
protected ZeroCrossingErrorDetector mTimingErrorDetector = new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL);
|
||||
protected SynchronizationMonitor mSynchronizationMonitor;
|
||||
private IIRSinglePoleDCRemovalFilter mDCFilter = new IIRSinglePoleDCRemovalFilter(0.99999f);
|
||||
private IRealFilter mLowPassFilter = FilterFactory.getRealFilter(sLowPassFilterCoefficients);
|
||||
private MessageFramer mMessageFramer;
|
||||
|
||||
private boolean mSampleDecision;
|
||||
|
||||
/**
|
||||
* Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder
|
||||
*
|
||||
* @param messageLength in symbols
|
||||
* Constructs an instance
|
||||
*/
|
||||
public LTRDecoder(int messageLength)
|
||||
public LTRDecoder()
|
||||
{
|
||||
this(messageLength, new SampleBuffer(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN),
|
||||
new ZeroCrossingErrorDetector(SAMPLES_PER_SYMBOL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a Logic Trunked Radio sub-audible 300 baud FSK signaling decoder.
|
||||
*
|
||||
* @param messageLength in symbols
|
||||
* @param sampleBuffer to use for storing sample decisions
|
||||
* @param detector for symbol timing errors
|
||||
* Registers the listener to receive decoded symbols.
|
||||
* @param listener to register
|
||||
*/
|
||||
public LTRDecoder(int messageLength, SampleBuffer sampleBuffer, ZeroCrossingErrorDetector detector)
|
||||
public void setListener(Listener<boolean[]> listener)
|
||||
{
|
||||
mSynchronizationMonitor = new SynchronizationMonitor(messageLength);
|
||||
mSynchronizationMonitor.setListener(this);
|
||||
mTimingErrorDetector = detector;
|
||||
mSampleBuffer = sampleBuffer;
|
||||
mSampleBuffer.setTimingGain(mSymbolTimingGain);
|
||||
mSymbolListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the ISyncDetectedListener interface to be notified of message sync detection events.
|
||||
*
|
||||
* This allows the internal timing error detector to adjust symbol timing error gain levels according to the
|
||||
* message synchronization state to quickly adjust to initial signal streams or to reduce gain levels once
|
||||
* synchronization has been achieved.
|
||||
* Processes the demodulated 8 kHz audio samples to extract the 300-baud LTR signalling and delivers the decoded
|
||||
* symbol array to the registered listener.
|
||||
* @param samples to demodulate.
|
||||
*/
|
||||
@Override
|
||||
public void syncDetected(int bitErrors)
|
||||
public void receive(float[] samples)
|
||||
{
|
||||
mSynchronizationMonitor.syncDetected(bitErrors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncLost(int bitsProcessed)
|
||||
{
|
||||
//no-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
|
||||
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
|
||||
* adjusted and then each of the symbols are decoded using a simple majority decision.
|
||||
*
|
||||
* @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling.
|
||||
*/
|
||||
@Override
|
||||
public void receive(float[] buffer)
|
||||
{
|
||||
float[] dcFiltered = mDCFilter.filter(buffer);
|
||||
float[] lowPassFiltered = mLowPassFilter.filter(dcFiltered);
|
||||
|
||||
for(float sample : lowPassFiltered)
|
||||
if(mSymbolListener != null)
|
||||
{
|
||||
mSampleDecision = sample > 0.0;
|
||||
float[] filtered = mLowPassFilter.filter(samples);
|
||||
|
||||
mSampleBuffer.receive(mSampleDecision);
|
||||
mTimingErrorDetector.receive(mSampleDecision);
|
||||
int timingAdjust = 0;
|
||||
int samplesToSkip = 0;
|
||||
|
||||
if(mSampleBuffer.hasSymbol())
|
||||
float[] buffer = new float[filtered.length + OVERLAP];
|
||||
boolean[] symbols = new boolean[(int)((filtered.length + mBaudCounter) / BAUD_LENGTH)];
|
||||
int symbolPointer = 0;
|
||||
float slope = 0;
|
||||
|
||||
try
|
||||
{
|
||||
if(mMessageFramer != null)
|
||||
System.arraycopy(mResidual, 0, buffer, 0, mResidual.length);
|
||||
System.arraycopy(filtered, 0, buffer, mResidual.length, filtered.length);
|
||||
System.arraycopy(filtered, filtered.length - OVERLAP, mResidual, 0, OVERLAP);
|
||||
|
||||
for(int bufferPointer = 0; bufferPointer < samples.length; bufferPointer++)
|
||||
{
|
||||
mMessageFramer.process(mSampleBuffer.getSymbol());
|
||||
//Don't calculate slope if we're within the baud period following last symbol transition
|
||||
if(samplesToSkip > 0)
|
||||
{
|
||||
samplesToSkip--;
|
||||
}
|
||||
else
|
||||
{
|
||||
slope = calculateSlope(buffer, bufferPointer);
|
||||
|
||||
if(mSymbol)
|
||||
{
|
||||
//Loop for crest of slope at lowest point below the negative slope threshold for transition
|
||||
if(slope > mMaxSlope && mMaxSlope < -SLOPE_THRESHOLD)
|
||||
{
|
||||
mSymbol = false;
|
||||
|
||||
//Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 16 samples
|
||||
if(mBaudCounter < SYMBOL_TRANSITION_IDEAL_MIN)
|
||||
{
|
||||
timingAdjust = 1;
|
||||
}
|
||||
else if(mBaudCounter > SYMBOL_TRANSITION_IDEAL_MAX)
|
||||
{
|
||||
timingAdjust = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
timingAdjust = 0;
|
||||
}
|
||||
|
||||
mBaudCounter += timingAdjust;
|
||||
samplesToSkip = POST_TRANSITION_SAMPLES_TO_SKIP + timingAdjust;
|
||||
}
|
||||
else if(slope < mMaxSlope)
|
||||
{
|
||||
mMaxSlope = slope;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//Loop for crest of slope at the highest point above the positive threshold
|
||||
if(slope < mMaxSlope && mMaxSlope > SLOPE_THRESHOLD)
|
||||
{
|
||||
mSymbol = true;
|
||||
|
||||
//Coarse symbol timing adjust - ideal toggle is middle of baud, between 11 & 16 samples
|
||||
if(mBaudCounter < SYMBOL_TRANSITION_IDEAL_MIN)
|
||||
{
|
||||
timingAdjust = 1;
|
||||
}
|
||||
else if(mBaudCounter > SYMBOL_TRANSITION_IDEAL_MAX)
|
||||
{
|
||||
timingAdjust = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
timingAdjust = 0;
|
||||
}
|
||||
mBaudCounter += timingAdjust;
|
||||
samplesToSkip = 26 + timingAdjust;
|
||||
}
|
||||
else if(slope > mMaxSlope)
|
||||
{
|
||||
mMaxSlope = slope;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mBaudCounter++;
|
||||
|
||||
if(mBaudCounter > BAUD_LENGTH)
|
||||
{
|
||||
//Expand symbols array length if needed to account for symbol timing adjustments
|
||||
if(symbolPointer >= symbols.length)
|
||||
{
|
||||
symbols = Arrays.copyOf(symbols, symbols.length + 1);
|
||||
}
|
||||
|
||||
symbols[symbolPointer++] = mSymbol;
|
||||
|
||||
mBaudCounter -= BAUD_LENGTH;
|
||||
|
||||
//Check for a continuous string of ones and flip the symbol state to zero.
|
||||
if(mSymbol)
|
||||
{
|
||||
mExcessiveOneSequenceCounter++;
|
||||
|
||||
if(mExcessiveOneSequenceCounter > MAX_ONES_SEQUENCE) //Verify that this is the right threshold for a string of ones
|
||||
{
|
||||
mSymbol = false;
|
||||
mMaxSlope = -1f;
|
||||
mExcessiveOneSequenceCounter = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mExcessiveOneSequenceCounter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.warn("Unexpected error while processing LTR samples", e);
|
||||
}
|
||||
|
||||
mSampleBuffer.resetAndAdjust(-mTimingErrorDetector.getError());
|
||||
|
||||
mSynchronizationMonitor.increment();
|
||||
//Truncate the symbols array if we didn't fill it completely, due to symbol timing adjustments.
|
||||
if(symbolPointer != symbols.length)
|
||||
{
|
||||
mSymbolListener.receive(Arrays.copyOf(symbols, symbolPointer));
|
||||
}
|
||||
else
|
||||
{
|
||||
mSymbolListener.receive(symbols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener to receive decoded LTR symbols.
|
||||
*
|
||||
* @param messageFramer to receive symbols.
|
||||
* Calculates the slope of the 27 samples in the values array starting at the specified offset.
|
||||
* @param samples to load from
|
||||
* @param offset to the sample start for the slope calculation
|
||||
*/
|
||||
public void setMessageFramer(MessageFramer messageFramer)
|
||||
public float calculateSlope(float[] samples, int offset)
|
||||
{
|
||||
mMessageFramer = messageFramer;
|
||||
}
|
||||
/** mean of accumulated x & y values, used in updating formulas */
|
||||
double xbar = 0;
|
||||
double ybar = samples[offset];
|
||||
|
||||
/**
|
||||
* Removes the symbol listener from receiving decoded LTR symbols.
|
||||
*/
|
||||
public void removeListener()
|
||||
{
|
||||
mMessageFramer = null;
|
||||
}
|
||||
double sumXY = 0;
|
||||
double fact1, dx, dy;
|
||||
|
||||
|
||||
/**
|
||||
* Implements the ISyncStateListener interface to receive synchronization state events and adjust the symbol timing
|
||||
* error gain levels on the internal timing error detector.
|
||||
*
|
||||
* Gain levels are defined relative to a unit gain of 1.0 over a number of symbol periods. LTR uses an initial
|
||||
* ramp-up symbol reversal pattern of 0101 which provides three zero crossing detection opportunities for the timing
|
||||
* error detector, therefore we react to a COARSE gain state to average timing error over 3 symbols.
|
||||
*
|
||||
* @param syncState from an external synchronization state monitor
|
||||
*/
|
||||
@Override
|
||||
public void setSyncState(SyncState syncState)
|
||||
{
|
||||
switch(syncState)
|
||||
for(int x = 1; x < SLOPE_CALCULATION_LENGTH; x++)
|
||||
{
|
||||
case FINE:
|
||||
mSymbolTimingGain = FINE_TIMING_GAIN;
|
||||
break;
|
||||
case MEDIUM:
|
||||
mSymbolTimingGain = MEDIUM_TIMING_GAIN;
|
||||
break;
|
||||
case COARSE:
|
||||
mSymbolTimingGain = COARSE_TIMING_GAIN;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized sync state level: " + syncState.name());
|
||||
fact1 = 1.0f + x;
|
||||
dx = x - xbar;
|
||||
dy = samples[offset + x] - ybar;
|
||||
sumXY += dx * dy * (x / (1.0f + x));
|
||||
xbar += dx / fact1;
|
||||
ybar += dy / fact1;
|
||||
}
|
||||
|
||||
mSampleBuffer.setTimingGain(mSymbolTimingGain);
|
||||
return (float)(sumXY / SLOPE_CALCULATION_SUM_XX);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +1,30 @@
|
||||
/*******************************************************************************
|
||||
* sdr-trunk
|
||||
* Copyright (C) 2014-2018 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2023 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
|
||||
* later version.
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with this program.
|
||||
* If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.fsk;
|
||||
|
||||
public class LTRDecoderInstrumented extends LTRDecoder
|
||||
{
|
||||
/**
|
||||
* Instrumented extension to LTRDecoder
|
||||
*
|
||||
* @param messageLength in symbols
|
||||
*/
|
||||
public LTRDecoderInstrumented(int messageLength)
|
||||
public LTRDecoderInstrumented()
|
||||
{
|
||||
super(messageLength, new SampleBufferInstrumented(SAMPLES_PER_SYMBOL, COARSE_TIMING_GAIN),
|
||||
new ZeroCrossingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL));
|
||||
}
|
||||
|
||||
public SampleBufferInstrumented getSampleBuffer()
|
||||
{
|
||||
return (SampleBufferInstrumented)mSampleBuffer;
|
||||
}
|
||||
|
||||
public ZeroCrossingErrorDetectorInstrumented getErrorDetector()
|
||||
{
|
||||
return (ZeroCrossingErrorDetectorInstrumented)mTimingErrorDetector;
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
* Copyright (C) 2014-2024 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
package io.github.dsheirer.dsp.oscillator;
|
||||
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
@@ -45,7 +46,15 @@ public class VectorRealOscillator extends AbstractOscillator implements IRealOsc
|
||||
@Override
|
||||
public float[] generate(int sampleCount)
|
||||
{
|
||||
float[] samples = new float[sampleCount];
|
||||
int length = sampleCount;
|
||||
|
||||
//Increase the length until it aligns as a multiple of the vector species length
|
||||
while(length % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
|
||||
float[] samples = new float[length];
|
||||
|
||||
for(int samplePointer = 0; samplePointer < sampleCount; samplePointer += VECTOR_SPECIES.length())
|
||||
{
|
||||
@@ -58,6 +67,7 @@ public class VectorRealOscillator extends AbstractOscillator implements IRealOsc
|
||||
generated.intoArray(samples, samplePointer);
|
||||
}
|
||||
|
||||
return samples;
|
||||
//Truncate the array to the requested length, if we increased it for SIMD alignment.
|
||||
return Arrays.copyOf(samples, sampleCount);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user