mirror of
https://github.com/DSheirer/sdrtrunk.git
synced 2026-11-10 15:47:25 -05:00
Compare commits
23 Commits
v0.5.0-bet
...
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4a85a4ed7 | ||
|
|
f61a415b43 | ||
|
|
29ed3b4553 | ||
|
|
a87309e792 | ||
|
|
ddb5b5ef87 | ||
|
|
7a58bf85b6 | ||
|
|
8707eb2c88 | ||
|
|
08499ac73f | ||
|
|
70170ff70f | ||
|
|
bad5589c0d | ||
|
|
7de28519a1 | ||
|
|
bbd907514e | ||
|
|
4a68cd9407 | ||
|
|
dc327a2855 | ||
|
|
9af593a785 | ||
|
|
24f4c22cdd | ||
|
|
bc462b9652 | ||
|
|
047dec6da1 | ||
|
|
bf935555bb | ||
|
|
efe36b0682 | ||
|
|
a18d4498aa | ||
|
|
803dc2531c | ||
|
|
1b062bcd06 |
81
README.md
81
README.md
@@ -3,91 +3,22 @@
|
||||
# sdrtrunk
|
||||
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
|
||||
|
||||
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/GettingStarted_V0.3.0)
|
||||
* [User's Manual Version 0.3.0 and 0.4.0](https://github.com/DSheirer/sdrtrunk/wiki/UserManual_V0.3.0)
|
||||
* [Playlist Editor User Manual Version 0.5.0](https://github.com/DSheirer/sdrtrunk/wiki/Playlist-Editor)
|
||||
* [Getting Started](https://github.com/DSheirer/sdrtrunk/wiki/Getting-Started)
|
||||
* [User's Manual](https://github.com/DSheirer/sdrtrunk/wiki/User-Manual)
|
||||
* [Download](https://github.com/DSheirer/sdrtrunk/releases)
|
||||
* [Support Group](https://groups.google.com/forum/#!forum/sdrtrunk)
|
||||
* [Discord Support Channel](https://discord.gg/HJQaKYE)
|
||||
* [Support](https://github.com/DSheirer/sdrtrunk/wiki/Support)
|
||||
|
||||

|
||||
**Figure 1:** sdrtrunk **Version 0.3.0** Application Screenshot
|
||||

|
||||
**Figure 1:** sdrtrunk Application Screenshot
|
||||
|
||||
# End User Instructions:
|
||||
|
||||
If you simply want to download and run the program, please follow these instructions.
|
||||
|
||||
## Download the latest sdrtrunk release for your operating system
|
||||
|
||||
## Download the Latest Release
|
||||
All release versions of sdrtrunk are available from the [releases](https://github.com/DSheirer/sdrtrunk/releases) tab.
|
||||
|
||||
* **(alpha)** These versions are under development feature previews and likely to contain bugs and unexpected behavior.
|
||||
* **(beta)** These versions are currently being tested for bugs and functionality prior to final release.
|
||||
* **(final)** These versions have been tested and are the current release version.
|
||||
|
||||
## Unzip the release
|
||||
|
||||
Use 7-zip or any zip utility to unzip the release file
|
||||
|
||||
## Start the application
|
||||
|
||||
Once unzipped, open a command prompt to where you unzipped the release. Change to the **/bin** directory and use the launch script to start the application:
|
||||
* **Windows** sdr-trunk.bat
|
||||
* **Linux/OSX** ./sdr-trunk
|
||||
|
||||
## Optional - P25/DMR Audio
|
||||
If you're using sdrtrunk with a P25 or DMR radio system, you'll need to compile and include the [JMBE](https://github.com/DSheirer/sdrtrunk/wiki/JMBE) digital audio library.
|
||||
|
||||
In Version 0.5.0 and higher, the SDRTrunk application has support for building the JMBE library. On the menu, choose **View >> User Preferences** and then click the **Decoder >> JMBE Audio Library** tab.
|
||||
|
||||
For versions prior to Version 0.5.0, the [JMBE](https://github.com/DSheirer/sdrtrunk/wiki/JMBE) wiki page contains instructions for downloading the JMBE audio library source code and compiling the JMBE library. Once you have compiled the library, launch the sdrtrunk application. From the menu bar, choose **View >> Preferences**. In the **JMBE Audio Codec** section, update the path to where your compiled JMBE library is located. Any channels that are started after you set the path will be able to produce P25 audio.
|
||||
|
||||
## Minimum System Requirements
|
||||
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
|
||||
* **CPU:** 4-core
|
||||
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.
|
||||
|
||||
# Developer Instructions:
|
||||
|
||||
If you're interested in modifying and/or compiling the source code, please follow these instructions to use gradle to compile the code.
|
||||
|
||||
## Build the project
|
||||
sdrtrunk uses the gradle build system. This requires **OpenJDK 17** or higher installed on your local computer.
|
||||
|
||||
### OpenJDK
|
||||
There are many vendors producing OpenJDK distributions. A few that I use are:
|
||||
* [Azul Zulu OpenJDK](https://www.azul.com/downloads/?package=jdk)
|
||||
* [Bellsoft Liberica OpenJDK](https://bell-sw.com/pages/downloads/#/java-17-lts%20/%20current)
|
||||
* [SDKMAN](https://sdkman.io/) - Linux Only
|
||||
|
||||
Use the gradle wrapper to build the source code:
|
||||
|
||||
### Linux
|
||||
```
|
||||
./gradlew clean build
|
||||
```
|
||||
### Windows
|
||||
```
|
||||
gradlew.bat clean build
|
||||
```
|
||||
|
||||
The **/build/distributions** folder will contain the zip file of the compiled program. Unzip it and launch the program from the scripts in the **/bin** directory.
|
||||
|
||||
## Run the Application (from Gradle)
|
||||
|
||||
### Linux
|
||||
```
|
||||
./gradlew clean run
|
||||
```
|
||||
### Windows
|
||||
```
|
||||
gradlew.bat clean run
|
||||
```
|
||||
|
||||
|
||||
## Development
|
||||
All dependencies/versions are controlled from build.gradle.
|
||||
To change the new release version tag of artifact - change property:
|
||||
```
|
||||
version = '0.5.0'
|
||||
```
|
||||
|
||||
12
build.gradle
12
build.gradle
@@ -28,7 +28,7 @@ repositories {
|
||||
maven { url "https://jitpack.io" }
|
||||
}
|
||||
|
||||
version = '0.5.0-beta6'
|
||||
version = '0.5.0'
|
||||
sourceCompatibility = '17'
|
||||
|
||||
sourceSets {
|
||||
@@ -56,10 +56,10 @@ dependencies {
|
||||
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
|
||||
|
||||
//mavenCentral/jcenter imports
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.4'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.4'
|
||||
implementation 'ch.qos.logback:logback-classic:1.4.5'
|
||||
implementation 'ch.qos.logback:logback-core:1.4.5'
|
||||
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
|
||||
implementation 'com.fazecast:jSerialComm:2.9.2'
|
||||
implementation 'com.fazecast:jSerialComm:2.9.3'
|
||||
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
|
||||
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
|
||||
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
|
||||
@@ -71,7 +71,7 @@ dependencies {
|
||||
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.8'
|
||||
implementation 'io.github.dsheirer:radio-reference-api:15.1.9'
|
||||
implementation 'javax.usb:usb-api:1.0.2'
|
||||
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
|
||||
implementation 'org.apache.commons:commons-compress:1.21'
|
||||
@@ -82,7 +82,7 @@ dependencies {
|
||||
implementation 'org.apache.mina:mina-http:2.2.1'
|
||||
implementation 'org.controlsfx:controlsfx:11.1.2'
|
||||
implementation 'org.rauschig:jarchivelib:1.2.0'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.3'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.5'
|
||||
implementation 'org.usb4java:libusb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java:1.3.0'
|
||||
implementation 'org.usb4java:usb4java-javax:1.3.0'
|
||||
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -19,6 +19,7 @@
|
||||
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-rc-3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
18
gradlew
vendored
18
gradlew
vendored
@@ -58,7 +58,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -83,10 +83,10 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
@@ -146,12 +146,16 @@ fi
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -208,6 +212,12 @@ set -- \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
|
||||
15
gradlew.bat
vendored
15
gradlew.bat
vendored
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,8 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -18,24 +18,22 @@
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.audio.convert.AudioFrames;
|
||||
import io.github.dsheirer.audio.convert.ISilenceGenerator;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3FrameTools;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration> extends AbstractAudioBroadcaster<T>
|
||||
{
|
||||
@@ -48,6 +46,7 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
private Queue<AudioRecording> mAudioRecordingQueue = new LinkedTransferQueue<>();
|
||||
private ISilenceGenerator mSilenceGenerator;
|
||||
|
||||
private BroadcastFormat mBroadcastFormat;
|
||||
private long mDelay;
|
||||
private long mMaximumRecordingAge;
|
||||
private AtomicBoolean mStreaming = new AtomicBoolean();
|
||||
@@ -56,6 +55,8 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
protected int mInlineInterval;
|
||||
protected int mInlineRemaining = -1;
|
||||
|
||||
private int mTimeOverrun = 0;
|
||||
|
||||
/**
|
||||
* AudioBroadcaster for streaming audio recordings to a remote streaming audio server. Audio recordings are
|
||||
* generated by an internal StreamManager that converts an inbound stream of AudioPackets into a recording of the
|
||||
@@ -80,10 +81,11 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
public AudioStreamingBroadcaster(T broadcastConfiguration, InputAudioFormat inputAudioFormat, MP3Setting mp3Setting)
|
||||
{
|
||||
super(broadcastConfiguration);
|
||||
mBroadcastFormat = broadcastConfiguration.getBroadcastFormat();
|
||||
mDelay = getBroadcastConfiguration().getDelay();
|
||||
mMaximumRecordingAge = getBroadcastConfiguration().getMaximumRecordingAge();
|
||||
mSilenceGenerator = BroadcastFactory.getSilenceGenerator(broadcastConfiguration.getBroadcastFormat(),
|
||||
inputAudioFormat, mp3Setting);
|
||||
inputAudioFormat, mp3Setting);
|
||||
}
|
||||
|
||||
public void dispose()
|
||||
@@ -249,7 +251,6 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
public boolean canConnect()
|
||||
{
|
||||
BroadcastState state = getBroadcastState();
|
||||
|
||||
return state != BroadcastState.CONNECTED && !state.isErrorState();
|
||||
}
|
||||
|
||||
@@ -270,10 +271,8 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
public class RecordingQueueProcessor implements Runnable
|
||||
{
|
||||
private AtomicBoolean mProcessing = new AtomicBoolean();
|
||||
private ByteArrayInputStream mInputStream;
|
||||
private AudioFrames mInputFrames;
|
||||
private IdentifierCollection mInputIdentifierCollection;
|
||||
private long mFinalSilencePadding = 0;
|
||||
private int mBytesToStreamPerInterval = 0;
|
||||
|
||||
@Override
|
||||
public void run()
|
||||
@@ -282,48 +281,35 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
{
|
||||
try
|
||||
{
|
||||
if(mInputStream == null || mInputStream.available() <= 0)
|
||||
int timeSent = 0;
|
||||
|
||||
if(mInputFrames == null || !mInputFrames.hasNextFrame())
|
||||
{
|
||||
nextRecording();
|
||||
}
|
||||
|
||||
if(mInputStream != null)
|
||||
if(mInputFrames != null && mInputFrames.hasNextFrame())
|
||||
{
|
||||
int length = FastMath.min(mBytesToStreamPerInterval, mInputStream.available());
|
||||
|
||||
byte[] audio = new byte[length];
|
||||
|
||||
try
|
||||
while(mInputFrames.hasNextFrame() && timeSent < PROCESSOR_RUN_INTERVAL_MS)
|
||||
{
|
||||
int read = mInputStream.read(audio);
|
||||
broadcastAudio(audio, mInputIdentifierCollection);
|
||||
}
|
||||
catch(IOException ioe)
|
||||
{
|
||||
mLog.error("Error reading from in-memory audio recording input stream", ioe);
|
||||
}
|
||||
|
||||
//If this is the final frame fragment then append silence padding to fill the final interval segment
|
||||
if(length < mBytesToStreamPerInterval && mFinalSilencePadding > 0)
|
||||
{
|
||||
List<byte[]> finalSilence = mSilenceGenerator.generate(mFinalSilencePadding);
|
||||
|
||||
for(byte[] silence: finalSilence)
|
||||
{
|
||||
broadcastAudio(silence, null);
|
||||
}
|
||||
|
||||
mFinalSilencePadding = 0;
|
||||
mInputFrames.nextFrame();
|
||||
broadcastAudio(mInputFrames.getCurrentFrame(), mInputIdentifierCollection);
|
||||
timeSent += mInputFrames.getCurrentFrameDuration();
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if((mInputFrames == null || !mInputFrames.hasNextFrame()) && timeSent < PROCESSOR_RUN_INTERVAL_MS)
|
||||
{
|
||||
List<byte[]> silenceFrames = mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS);
|
||||
for(byte[] silence: silenceFrames)
|
||||
AudioFrames silenceFrames = mSilenceGenerator.generate(PROCESSOR_RUN_INTERVAL_MS - mTimeOverrun - timeSent);
|
||||
while(silenceFrames.hasNextFrame())
|
||||
{
|
||||
broadcastAudio(silence, null);
|
||||
silenceFrames.nextFrame();
|
||||
broadcastAudio(silenceFrames.getCurrentFrame(), null);
|
||||
timeSent += silenceFrames.getCurrentFrameDuration();
|
||||
}
|
||||
}
|
||||
|
||||
mTimeOverrun += timeSent - PROCESSOR_RUN_INTERVAL_MS;
|
||||
}
|
||||
catch(Throwable t)
|
||||
{
|
||||
@@ -341,7 +327,7 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
{
|
||||
boolean metadataUpdateRequired = false;
|
||||
|
||||
if(mInputStream != null)
|
||||
if(mInputFrames != null)
|
||||
{
|
||||
mStreamedAudioCount++;
|
||||
broadcast(new BroadcastEvent(AudioStreamingBroadcaster.this,
|
||||
@@ -349,7 +335,7 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
metadataUpdateRequired = true;
|
||||
}
|
||||
|
||||
mInputStream = null;
|
||||
mInputFrames = null;
|
||||
mInputIdentifierCollection = null;
|
||||
|
||||
//Peek at the next recording but don't remove it from the queue yet, so we can inspect the start time for
|
||||
@@ -380,20 +366,15 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
|
||||
if(audio.length > 0)
|
||||
{
|
||||
//Calculate how many bytes we'll send with each processor run interval
|
||||
double intervals = (double)nextRecording.getRecordingLength() / (double)PROCESSOR_RUN_INTERVAL_MS;
|
||||
mBytesToStreamPerInterval = (int)((double)audio.length / intervals);
|
||||
|
||||
mInputStream = new ByteArrayInputStream(audio);
|
||||
mInputIdentifierCollection = nextRecording.getIdentifierCollection();
|
||||
|
||||
mFinalSilencePadding = PROCESSOR_RUN_INTERVAL_MS -
|
||||
(nextRecording.getRecordingLength() % PROCESSOR_RUN_INTERVAL_MS);
|
||||
|
||||
while(mFinalSilencePadding >= PROCESSOR_RUN_INTERVAL_MS)
|
||||
switch(mBroadcastFormat)
|
||||
{
|
||||
mFinalSilencePadding -= PROCESSOR_RUN_INTERVAL_MS;
|
||||
case MP3:
|
||||
mInputFrames = MP3FrameTools.split(audio);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported broadcast format [" + mBroadcastFormat + "]");
|
||||
}
|
||||
mInputIdentifierCollection = nextRecording.getIdentifierCollection();
|
||||
|
||||
if(connected())
|
||||
{
|
||||
@@ -409,7 +390,7 @@ public abstract class AudioStreamingBroadcaster<T extends BroadcastConfiguration
|
||||
mLog.error("Stream [" + getBroadcastConfiguration().getName() + "] error reading temporary audio " +
|
||||
"stream recording [" + nextRecording.getPath().toString() + "] - skipping recording - ", ioe);
|
||||
|
||||
mInputStream = null;
|
||||
mInputFrames = null;
|
||||
mInputIdentifierCollection = null;
|
||||
metadataUpdateRequired = false;
|
||||
}
|
||||
|
||||
@@ -26,13 +26,6 @@ import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
@@ -48,6 +41,12 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
|
||||
public class BroadcastModel extends AbstractTableModel implements Listener<AudioRecording>
|
||||
{
|
||||
@@ -58,7 +57,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
|
||||
private static final String UNIQUE_NAME_REGEX = "(.*)\\((\\d*)\\)";
|
||||
|
||||
public static final int COLUMN_SERVER_ICON = 0;
|
||||
public static final int COLUMN_BROADCAST_SERVER_TYPE = 0;
|
||||
public static final int COLUMN_STREAM_NAME = 1;
|
||||
public static final int COLUMN_BROADCASTER_STATUS = 2;
|
||||
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
|
||||
@@ -67,7 +66,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 6;
|
||||
|
||||
public static final String[] COLUMN_NAMES = new String[]
|
||||
{"Streaming", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
|
||||
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
|
||||
|
||||
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
|
||||
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
|
||||
@@ -575,15 +574,8 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
{
|
||||
switch(columnIndex)
|
||||
{
|
||||
case COLUMN_SERVER_ICON:
|
||||
String iconPath = configuredBroadcast.getBroadcastConfiguration()
|
||||
.getBroadcastServerType().getIconPath();
|
||||
|
||||
if(iconPath != null && mIconModel != null)
|
||||
{
|
||||
return mIconModel.getScaledIcon(iconPath, 14);
|
||||
}
|
||||
break;
|
||||
case COLUMN_BROADCAST_SERVER_TYPE:
|
||||
return configuredBroadcast.getBroadcastServerType();
|
||||
case COLUMN_STREAM_NAME:
|
||||
return configuredBroadcast.getBroadcastConfiguration().getName();
|
||||
case COLUMN_BROADCASTER_STATUS:
|
||||
@@ -653,8 +645,8 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
|
||||
case COLUMN_BROADCASTER_STREAMED_COUNT:
|
||||
case COLUMN_BROADCASTER_ERROR_COUNT:
|
||||
return Integer.class;
|
||||
case COLUMN_SERVER_ICON:
|
||||
return ImageIcon.class;
|
||||
case COLUMN_BROADCAST_SERVER_TYPE:
|
||||
return BroadcastServerType.class;
|
||||
case COLUMN_STREAM_NAME:
|
||||
default:
|
||||
return String.class;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -14,8 +14,8 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
public enum BroadcastServerType
|
||||
@@ -23,17 +23,17 @@ public enum BroadcastServerType
|
||||
/**
|
||||
* Broadcastify feeds (ie streaming) service
|
||||
*/
|
||||
BROADCASTIFY("Broadcastify Feed", "/images/broadcastify.png"), //Icecast Server 2.3.2
|
||||
BROADCASTIFY("Broadcastify Feed", "images/broadcastify.png"), //Icecast Server 2.3.2
|
||||
|
||||
/**
|
||||
* Broadcastify calls - completed audio recording push service
|
||||
*/
|
||||
BROADCASTIFY_CALL("Broadcastify Call", "/images/broadcastify.png"),
|
||||
BROADCASTIFY_CALL("Broadcastify Call", "images/broadcastify.png"),
|
||||
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "/images/icecast.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "/images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "/images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "/images/shoutcast.png"),
|
||||
ICECAST_HTTP("Icecast 2 (v2.4+)", "images/icecast.png"),
|
||||
ICECAST_TCP("Icecast (v2.3)", "images/icecast.png"),
|
||||
SHOUTCAST_V1("Shoutcast v1.x", "images/shoutcast.png"),
|
||||
SHOUTCAST_V2("Shoutcast v2.x", "images/shoutcast.png"),
|
||||
UNKNOWN("Unknown", null);
|
||||
|
||||
private String mLabel;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2016 Dennis Sheirer
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -14,8 +14,8 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
******************************************************************************/
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
public enum BroadcastState
|
||||
@@ -68,7 +68,7 @@ public enum BroadcastState
|
||||
/**
|
||||
* Specified mount point is already in use
|
||||
*/
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", false),
|
||||
MOUNT_POINT_IN_USE("Mount Point In Use", true),
|
||||
|
||||
/**
|
||||
* Network is unavailable or the server address cannot be resolved
|
||||
@@ -103,10 +103,10 @@ public enum BroadcastState
|
||||
private String mLabel;
|
||||
private boolean mErrorState;
|
||||
|
||||
private BroadcastState(String label, boolean connected)
|
||||
BroadcastState(String label, boolean errorState)
|
||||
{
|
||||
mLabel = label;
|
||||
mErrorState = connected;
|
||||
mErrorState = errorState;
|
||||
}
|
||||
|
||||
public String toString()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/*
|
||||
* ******************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,23 +14,29 @@
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* *****************************************************************************
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.broadcast;
|
||||
|
||||
import io.github.dsheirer.icon.Icon;
|
||||
import io.github.dsheirer.icon.IconModel;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.SwingConstants;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
|
||||
/**
|
||||
* Table of broadcast streams and statuses.
|
||||
*/
|
||||
public class BroadcastStatusPanel extends JPanel
|
||||
{
|
||||
private JTable mTable;
|
||||
@@ -41,6 +46,12 @@ public class BroadcastStatusPanel extends JPanel
|
||||
private UserPreferences mUserPreferences;
|
||||
private String mPreferenceKey;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param broadcastModel to access the streams
|
||||
* @param userPreferences for configuring the panel
|
||||
* @param preferenceKey to store column preferences for this panel.
|
||||
*/
|
||||
public BroadcastStatusPanel(BroadcastModel broadcastModel, UserPreferences userPreferences, String preferenceKey)
|
||||
{
|
||||
mBroadcastModel = broadcastModel;
|
||||
@@ -65,6 +76,7 @@ public class BroadcastStatusPanel extends JPanel
|
||||
renderer.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
|
||||
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCASTER_STATUS).setCellRenderer(new StatusCellRenderer());
|
||||
mTable.getColumnModel().getColumn(BroadcastModel.COLUMN_BROADCAST_SERVER_TYPE).setCellRenderer(new ServerTypeRenderer());
|
||||
mColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, mPreferenceKey);
|
||||
|
||||
mScrollPane = new JScrollPane(mTable);
|
||||
@@ -72,6 +84,37 @@ public class BroadcastStatusPanel extends JPanel
|
||||
add(mScrollPane);
|
||||
}
|
||||
|
||||
public class ServerTypeRenderer extends DefaultTableCellRenderer
|
||||
{
|
||||
public ServerTypeRenderer()
|
||||
{
|
||||
setOpaque(true);
|
||||
setHorizontalAlignment(SwingConstants.CENTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
|
||||
{
|
||||
JLabel component = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
|
||||
if(value instanceof BroadcastServerType broadcastServerType)
|
||||
{
|
||||
component.setText(broadcastServerType.toString());
|
||||
Icon icon = new Icon("empty", broadcastServerType.getIconPath());
|
||||
ImageIcon imageIcon = icon.getIcon();
|
||||
ImageIcon scaledIcon = IconModel.getScaledIcon(imageIcon, 13);
|
||||
component.setIcon(scaledIcon);
|
||||
}
|
||||
else
|
||||
{
|
||||
component.setText(null);
|
||||
component.setIcon(null);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom cell renderer for the broadcast state column.
|
||||
*/
|
||||
|
||||
@@ -20,13 +20,20 @@ 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.icecast.IcecastMetadata;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
@@ -42,13 +49,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
|
||||
@@ -143,8 +143,9 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
mSocketConnector = new NioSocketConnector();
|
||||
mSocketConnector.setConnectTimeoutCheckInterval(10000);
|
||||
|
||||
// mSocketConnector.getFilterChain().addLast("logger",
|
||||
// new LoggingFilter(IcecastHTTPAudioBroadcaster.class));
|
||||
// LoggingFilter loggingFilter = new LoggingFilter(IcecastHTTPAudioBroadcaster.class);
|
||||
// loggingFilter.setMessageReceivedLogLevel(LogLevel.DEBUG);
|
||||
// mSocketConnector.getFilterChain().addLast("logger", loggingFilter);
|
||||
|
||||
mSocketConnector.getFilterChain().addLast("codec", new HttpClientCodec());
|
||||
mSocketConnector.setHandler(new IcecastHTTPIOHandler());
|
||||
@@ -315,10 +316,49 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
* intercept the out of bounds exception and then inspect the message hex dump to see if it's an
|
||||
* OK response and then play like everything is well and good.
|
||||
*/
|
||||
if(cause instanceof ArrayIndexOutOfBoundsException &&
|
||||
((ProtocolDecoderException)throwable).getHexdump().startsWith(HTTP_1_0_OK_HEX_DUMP))
|
||||
if(cause instanceof ArrayIndexOutOfBoundsException)
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
String hexDump = ((ProtocolDecoderException)throwable).getHexdump();
|
||||
|
||||
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
|
||||
{
|
||||
setBroadcastState(BroadcastState.CONNECTED);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpDumpMessage message = new HttpDumpMessage(hexDump);
|
||||
|
||||
if(message.hasHttpResponseCode())
|
||||
{
|
||||
switch(message.getHttpResponseCode())
|
||||
{
|
||||
case 403: //Forbidden
|
||||
if(message.toString().contains("Mountpoint in use"))
|
||||
{
|
||||
mLog.error("Stream [" + getStreamName() + "] - unable to connect - mountpoint in use");
|
||||
setBroadcastState(BroadcastState.MOUNT_POINT_IN_USE);
|
||||
disconnect();
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.error("String [" + getStreamName() + "] - HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.error("HTTP protocol decoder error - message:\n\n" + message);
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -374,4 +414,97 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for parsing HTTP response messages from Icecast 2.4.2+
|
||||
*/
|
||||
public class HttpDumpMessage
|
||||
{
|
||||
private String mHexDump;
|
||||
private String mMessage;
|
||||
private int mHttpResponseCode = -1;
|
||||
|
||||
public HttpDumpMessage(String hexDump)
|
||||
{
|
||||
mHexDump = hexDump;
|
||||
|
||||
String[] split = mHexDump.split(" ");
|
||||
byte[] bytes = new byte[split.length];
|
||||
|
||||
int pointer = 0;
|
||||
for(String a : split)
|
||||
{
|
||||
try
|
||||
{
|
||||
int value = Integer.parseInt(a, 16);
|
||||
bytes[pointer++] = (byte) (value & 0xFF);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
pointer++;
|
||||
}
|
||||
}
|
||||
|
||||
mMessage = new String(bytes);
|
||||
|
||||
Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
|
||||
Matcher m = pattern.matcher(mMessage);
|
||||
|
||||
if(m.find())
|
||||
{
|
||||
try
|
||||
{
|
||||
mHttpResponseCode = Integer.parseInt(m.group(1));
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
mLog.error("Unable to parse HTTP response code that was matched from message: " + m.group(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if an HTTP response code was parsed from the message
|
||||
* @return true if it was parsed.
|
||||
*/
|
||||
public boolean hasHttpResponseCode()
|
||||
{
|
||||
return mHttpResponseCode != -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response code parsed from the hexdump message
|
||||
* @return value.
|
||||
*/
|
||||
public int getHttpResponseCode()
|
||||
{
|
||||
return mHttpResponseCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return mMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
String text = "HTTP/1.0 403 Forbidden";
|
||||
|
||||
Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
|
||||
|
||||
Matcher m = pattern.matcher(text);
|
||||
|
||||
if(m.find())
|
||||
{
|
||||
mLog.info("Matching Group Count: " + m.groupCount());
|
||||
mLog.info("Match 0: " + m.group(0));
|
||||
mLog.info("Match 1: " + m.group(1));
|
||||
}
|
||||
else
|
||||
{
|
||||
mLog.info("No Match");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,19 @@ 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.icecast.codec.IcecastCodecFactory;
|
||||
import io.github.dsheirer.audio.broadcast.icecast.IcecastMetadata;
|
||||
import io.github.dsheirer.audio.convert.InputAudioFormat;
|
||||
import io.github.dsheirer.audio.convert.MP3AudioConverter;
|
||||
import io.github.dsheirer.audio.convert.MP3Setting;
|
||||
import io.github.dsheirer.identifier.IdentifierCollection;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.mina.core.RuntimeIoException;
|
||||
import org.apache.mina.core.future.ConnectFuture;
|
||||
import org.apache.mina.core.service.IoHandlerAdapter;
|
||||
@@ -37,14 +43,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(IcecastTCPAudioBroadcaster.class);
|
||||
@@ -215,7 +213,13 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
|
||||
}
|
||||
else
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
//Only set broadcast state to disconnected if it's not already in an error state, to prevent a restart. We
|
||||
//want to preserve the error state that got us here, so the user can see it.
|
||||
if(!getBroadcastState().isErrorState())
|
||||
{
|
||||
setBroadcastState(BroadcastState.DISCONNECTED);
|
||||
}
|
||||
|
||||
mLastConnectionAttempt = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
133
src/main/java/io/github/dsheirer/audio/convert/AudioFrames.java
Normal file
133
src/main/java/io/github/dsheirer/audio/convert/AudioFrames.java
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public abstract class AudioFrames
|
||||
{
|
||||
protected final int audioDuration;
|
||||
protected final List<byte[]> audioFrames;
|
||||
protected int mCurrentFrame = -1;
|
||||
|
||||
/**
|
||||
* Create new AudioFrame
|
||||
* @param audioDuration total duration in milliseconds
|
||||
* @param audioFrames series of audio frames, split on frame boundaries
|
||||
*/
|
||||
public AudioFrames(int audioDuration, List<byte[]> audioFrames)
|
||||
{
|
||||
this.audioDuration = audioDuration;
|
||||
this.audioFrames = audioFrames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total duration of frames
|
||||
* @return total duration in milliseconds
|
||||
*/
|
||||
public int getDuration()
|
||||
{
|
||||
return audioDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all frames
|
||||
* @return list of byte arrays, with each element representing a single frame of audio
|
||||
*/
|
||||
public List<byte[]> getFrames()
|
||||
{
|
||||
return audioFrames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current frame
|
||||
* @return byte array with the current frame of audio
|
||||
*/
|
||||
public byte[] getCurrentFrame()
|
||||
{
|
||||
return audioFrames.get(mCurrentFrame);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current frame duration
|
||||
* @return frame duration in milliseconds
|
||||
*/
|
||||
public abstract int getCurrentFrameDuration();
|
||||
|
||||
/**
|
||||
* Indicates if there is a frame before the current one
|
||||
* @return true if the frame exists, otherwise false
|
||||
*/
|
||||
public boolean hasPrevFrame()
|
||||
{
|
||||
int prevFrame = mCurrentFrame - 1;
|
||||
return prevFrame >= 0 && prevFrame < audioFrames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there is a frame after the current one
|
||||
* @return true if the frame exists, otherwise false
|
||||
*/
|
||||
public boolean hasNextFrame()
|
||||
{
|
||||
int nextFrame = mCurrentFrame + 1;
|
||||
return nextFrame >= 0 && nextFrame < audioFrames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to the previous frame
|
||||
*/
|
||||
public void prevFrame()
|
||||
{
|
||||
mCurrentFrame -= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to the next frame
|
||||
*/
|
||||
public void nextFrame()
|
||||
{
|
||||
mCurrentFrame += 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart from beginning
|
||||
* Must call nextFrame() to get first frame
|
||||
*/
|
||||
public void restart()
|
||||
{
|
||||
mCurrentFrame = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek by the specified time
|
||||
* @param duration_ms time in milliseconds, positive or negative
|
||||
*/
|
||||
public void seek(int duration_ms)
|
||||
{
|
||||
int step = duration_ms < 0 ? -1 : 1;
|
||||
int actual_ms = 0;
|
||||
while(FastMath.abs(actual_ms) < FastMath.abs(duration_ms))
|
||||
{
|
||||
mCurrentFrame += step;
|
||||
actual_ms += getCurrentFrameDuration();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,5 +22,5 @@ import java.util.List;
|
||||
|
||||
public interface ISilenceGenerator
|
||||
{
|
||||
List<byte[]> generate(long duration);
|
||||
AudioFrames generate(long duration);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.AudioFormats;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
|
||||
@@ -29,8 +30,8 @@ import javax.sound.sampled.AudioFormat;
|
||||
public enum InputAudioFormat
|
||||
{
|
||||
SR_8000(AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO, "16-Bit 8000 Hz (no resample)"),
|
||||
SR_16000(AudioFormats.PCM_SIGNED_16000_HZ_16_BIT_MONO, "16-Bit 16000 Hz"),
|
||||
SR_22050(AudioFormats.PCM_SIGNED_22050_HZ_16_BIT_MONO, "16-Bit 22050 Hz (default)"),
|
||||
SR_16000(AudioFormats.PCM_SIGNED_16000_HZ_16_BIT_MONO, "16-Bit 16000 Hz (default)"),
|
||||
SR_22050(AudioFormats.PCM_SIGNED_22050_HZ_16_BIT_MONO, "16-Bit 22050 Hz"),
|
||||
SR_44100(AudioFormats.PCM_SIGNED_44100_HZ_16_BIT_MONO, "16-Bit 44100 Hz"),
|
||||
|
||||
SR_32_8000(AudioFormats.PCM_SIGNED_8000_HZ_32_BIT_MONO, "32-Bit 8000 Hz (no resample)"),
|
||||
@@ -41,6 +42,17 @@ public enum InputAudioFormat
|
||||
private AudioFormat mAudioFormat;
|
||||
private String mLabel;
|
||||
|
||||
/**
|
||||
* Set that includes sample rates of 8 or 16 kHz
|
||||
*/
|
||||
public static final EnumSet<InputAudioFormat> SAMPLE_RATES_8_16 = EnumSet.of(SR_8000, SR_32_8000, SR_16000, SR_32_16000);
|
||||
|
||||
/**
|
||||
* Set that includes sample rates of 8, 16, or 22 kHz
|
||||
*/
|
||||
public static final EnumSet<InputAudioFormat> SAMPLE_RATES_8_16_22 = EnumSet.of(SR_8000, SR_32_8000, SR_16000,
|
||||
SR_32_16000, SR_22050, SR_32_22050);
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param audioFormat for the specified sample rate
|
||||
@@ -56,7 +68,7 @@ public enum InputAudioFormat
|
||||
*/
|
||||
public static InputAudioFormat getDefault()
|
||||
{
|
||||
return SR_22050;
|
||||
return SR_16000;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,8 +60,17 @@ public class MP3AudioConverter implements IAudioConverter
|
||||
mEncoder = LameFactory.getLameEncoder(inputAudioFormat, setting);
|
||||
mNormalizeAudio = normalizeAudio;
|
||||
|
||||
//Ensure input audio sample rate is supported for the MP3 setting and update as necessary - should never happen.
|
||||
if((mInputAudioFormat.getSampleRate() - mEncoder.getEffectiveSampleRate()) > 1.0)
|
||||
{
|
||||
mInputAudioFormat = InputAudioFormat.getDefault();
|
||||
mLog.warn("MP3 setting [" + setting + "] does not support requested input audio sample rate [" +
|
||||
inputAudioFormat + "] - using default sample rate [" + mInputAudioFormat + "]");
|
||||
mEncoder = LameFactory.getLameEncoder(inputAudioFormat, setting);
|
||||
}
|
||||
|
||||
//Resampling is only required if desired input sample rate is not system default of 8kHz
|
||||
if(inputAudioFormat != InputAudioFormat.SR_8000 && inputAudioFormat != InputAudioFormat.SR_32_8000)
|
||||
if(mInputAudioFormat != InputAudioFormat.SR_8000 && mInputAudioFormat != InputAudioFormat.SR_32_8000)
|
||||
{
|
||||
mResampler = LameFactory.getResampler(inputAudioFormat);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MP3AudioFrames extends AudioFrames
|
||||
{
|
||||
|
||||
/**
|
||||
* Create new MP3AudioFrame
|
||||
* @param audioDuration total duration in milliseconds
|
||||
* @param audioFrames series of audio frames, split on frame boundaries
|
||||
*/
|
||||
public MP3AudioFrames(int audioDuration, List<byte[]> audioFrames)
|
||||
{
|
||||
super(audioDuration, audioFrames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current frame duration
|
||||
* @return frame duration in milliseconds
|
||||
*/
|
||||
public int getCurrentFrameDuration()
|
||||
{
|
||||
return MP3Header.getFrameDuration(getCurrentFrame(), 0);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -55,6 +55,7 @@ public class MP3FrameInspector
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: Rework for MP3SilenceGenerator returning IAudioFrames
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.info("Starting ...");
|
||||
@@ -66,4 +67,5 @@ public class MP3FrameInspector
|
||||
|
||||
mLog.info("Finished");
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class MP3FrameTools {
|
||||
|
||||
private MP3FrameTools() {}
|
||||
|
||||
/**
|
||||
* Split MP3 audio at frame boundaries
|
||||
* @param input byte array of audio
|
||||
* @return list of byte arrays containing audio frames
|
||||
*/
|
||||
public static MP3AudioFrames split(byte[] input)
|
||||
{
|
||||
List<byte[]> mp3Frames = new ArrayList<>();
|
||||
int audioDuration = 0;
|
||||
|
||||
int offset = 0;
|
||||
while(offset < input.length)
|
||||
{
|
||||
if(MP3Header.hasSync(input, offset))
|
||||
{
|
||||
int framelen = MP3Header.getFrameLength(input, offset);
|
||||
mp3Frames.add(Arrays.copyOfRange(input, offset, offset + FastMath.min(framelen, input.length - offset)));
|
||||
audioDuration += MP3Header.getFrameDuration(input, offset);
|
||||
offset += framelen;
|
||||
}
|
||||
else
|
||||
{
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return new MP3AudioFrames(audioDuration, mp3Frames);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,10 @@
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import io.github.dsheirer.audio.convert.MPEGLayer;
|
||||
import io.github.dsheirer.audio.convert.MPEGVersion;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
/**
|
||||
* Utility class for inspecting/parsing MP3 frames
|
||||
*/
|
||||
@@ -88,6 +92,71 @@ public class MP3Header
|
||||
return ChannelMode.fromValue((frame[offset + 3] & 0xC0) >> 6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the frame has padding
|
||||
* @param frame
|
||||
* @param offset
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isPadded(byte[] frame, int offset)
|
||||
{
|
||||
return (frame[offset + 2] & 0x02) == 0x02;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of bytes in the frame
|
||||
* @param frame
|
||||
* @param offset
|
||||
* @return frame length in bytes
|
||||
*/
|
||||
public static int getFrameLength(byte[] frame, int offset)
|
||||
{
|
||||
return (getFrameSamples(frame, offset) / 8) * (getBitRate(frame, offset) * 1000) / getSampleRate(frame, offset) + (isPadded(frame, offset) ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of samples in the frame
|
||||
* @param frame
|
||||
* @param offset
|
||||
* @return length of frame in samples
|
||||
*/
|
||||
public static int getFrameSamples(byte[] frame, int offset)
|
||||
{
|
||||
switch(getMPEGVersion(frame, offset))
|
||||
{
|
||||
case V_1:
|
||||
switch(getMPEGLayer(frame, offset))
|
||||
{
|
||||
case LAYER1: return 384;
|
||||
case LAYER2: return 1152;
|
||||
case LAYER3: return 1152;
|
||||
default:
|
||||
}
|
||||
case V_2:
|
||||
case V_2_5:
|
||||
switch(getMPEGLayer(frame, offset))
|
||||
{
|
||||
case LAYER1: return 384;
|
||||
case LAYER2: return 1152;
|
||||
case LAYER3: return 576;
|
||||
default:
|
||||
}
|
||||
default:
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the frame duration in milliseconds
|
||||
* @param frame
|
||||
* @param offset
|
||||
* @return duration in milliseconds
|
||||
*/
|
||||
public static int getFrameDuration(byte[] frame, int offset)
|
||||
{
|
||||
return (int)FastMath.round((double)getFrameSamples(frame, offset) / (double)getSampleRate(frame, offset) * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the frame bytes and provides details about the header.
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import net.sourceforge.lame.mp3.Lame;
|
||||
|
||||
/**
|
||||
@@ -79,6 +81,30 @@ public enum MP3Setting
|
||||
return mSetting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Input audio formats that are supported by the MP3Setting value.
|
||||
* @return list of supported input audio sample rates.
|
||||
*/
|
||||
public List<InputAudioFormat> getSupportedSampleRates()
|
||||
{
|
||||
switch(this)
|
||||
{
|
||||
case CBR_16:
|
||||
{
|
||||
return InputAudioFormat.SAMPLE_RATES_8_16.stream().toList();
|
||||
}
|
||||
case CBR_32:
|
||||
case VBR_7:
|
||||
{
|
||||
return InputAudioFormat.SAMPLE_RATES_8_16_22.stream().toList();
|
||||
}
|
||||
default:
|
||||
{
|
||||
return Arrays.stream(InputAudioFormat.values()).toList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
||||
@@ -19,19 +19,20 @@
|
||||
package io.github.dsheirer.audio.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
|
||||
public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(MP3SilenceGenerator.class);
|
||||
public static final int MP3_BIT_RATE = 16;
|
||||
public static final boolean CONSTANT_BIT_RATE = false;
|
||||
|
||||
private MP3AudioConverter mMP3AudioConverter;
|
||||
private InputAudioFormat mInputAudioFormat;
|
||||
private byte[] mPreviousPartialFrameData;
|
||||
private byte[] mSilenceFrame;
|
||||
private int mSilenceFrameDuration;
|
||||
|
||||
/**
|
||||
* Generates MP3 audio silence frames
|
||||
@@ -40,19 +41,17 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
{
|
||||
mInputAudioFormat = inputAudioFormat;
|
||||
mMP3AudioConverter = new MP3AudioConverter(inputAudioFormat, setting, false);
|
||||
generate_frame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates silence frames
|
||||
* @param duration_ms in milliseconds
|
||||
* Generates a single silence frame
|
||||
* @return
|
||||
*/
|
||||
public List<byte[]> generate(long duration_ms)
|
||||
private void generate_frame()
|
||||
{
|
||||
double duration_secs = (double)duration_ms / 1000.0;
|
||||
|
||||
//We generate silence at 8000 kHz, because it gets resampled by the silence generator to the target rate
|
||||
int length = (int)(duration_secs * InputAudioFormat.SR_8000.getSampleRate());
|
||||
int length = (int)(InputAudioFormat.SR_8000.getSampleRate());
|
||||
|
||||
List<float[]> silenceBuffers = new ArrayList<>();
|
||||
|
||||
@@ -64,7 +63,40 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
added += chunk;
|
||||
}
|
||||
|
||||
return mMP3AudioConverter.convert(silenceBuffers);
|
||||
List<byte[]> mp3Audio = mMP3AudioConverter.convert(silenceBuffers);
|
||||
byte[] silence = new byte[0];
|
||||
for(byte[] mp3Chunk: mp3Audio)
|
||||
{
|
||||
silence = merge(silence, mp3Chunk);
|
||||
}
|
||||
|
||||
MP3AudioFrames mp3Frames = MP3FrameTools.split(silence);
|
||||
if(mp3Frames.hasNextFrame())
|
||||
{
|
||||
mp3Frames.nextFrame();
|
||||
mSilenceFrame = mp3Frames.getCurrentFrame();
|
||||
mSilenceFrameDuration = mp3Frames.getCurrentFrameDuration();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates silence frames
|
||||
* @param duration_ms in milliseconds
|
||||
* @return
|
||||
*/
|
||||
public MP3AudioFrames generate(long duration_ms)
|
||||
{
|
||||
List<byte[]> silenceFrames = new ArrayList<>();
|
||||
int silenceDuration = 0;
|
||||
|
||||
while(silenceDuration < duration_ms)
|
||||
{
|
||||
silenceFrames.add(mSilenceFrame);
|
||||
silenceDuration += mSilenceFrameDuration;
|
||||
}
|
||||
|
||||
return new MP3AudioFrames(silenceDuration, silenceFrames);
|
||||
}
|
||||
|
||||
private static byte[] merge(byte[] a, byte[] b)
|
||||
@@ -89,6 +121,7 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
return c;
|
||||
}
|
||||
|
||||
/* TODO: Rework for MP3SilenceGenerator returning IAudioFrames
|
||||
public static void main(String[] args)
|
||||
{
|
||||
mLog.debug("Starting ...");
|
||||
@@ -113,4 +146,5 @@ public class MP3SilenceGenerator implements ISilenceGenerator
|
||||
|
||||
mLog.debug("Finished");
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer;
|
||||
|
||||
/**
|
||||
* Base native buffer class.
|
||||
*/
|
||||
public abstract class AbstractNativeBuffer implements INativeBuffer
|
||||
{
|
||||
private long mTimestamp;
|
||||
private float mSamplesPerMillisecond;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param timestamp for the start of this buffer
|
||||
* @param samplesPerMillisecond to calculate the time offset of sub-buffers extracted from this native buffer.
|
||||
*/
|
||||
public AbstractNativeBuffer(long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
mTimestamp = timestamp;
|
||||
mSamplesPerMillisecond = samplesPerMillisecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp for the start of this buffer
|
||||
* @return timestamp in milliseconds
|
||||
*/
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quantity of samples representing one millisecond of sample data, used for calculating fragment timestamp offsets.
|
||||
* @return samples per millisecond count.
|
||||
*/
|
||||
public float getSamplesPerMillisecond()
|
||||
{
|
||||
return mSamplesPerMillisecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
|
||||
* native buffer starting timestamp.
|
||||
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
|
||||
* @return timestamp adjusted to the fragment or sub-buffer start sample.
|
||||
*/
|
||||
protected long getFragmentTimestamp(int samplesPointer)
|
||||
{
|
||||
return getTimestamp() + (long)(samplesPointer / 2 / getSamplesPerMillisecond());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.buffer;
|
||||
|
||||
/**
|
||||
* Base class for native buffer factories.
|
||||
*/
|
||||
public abstract class AbstractNativeBufferFactory implements INativeBufferFactory
|
||||
{
|
||||
private float mSamplesPerMillisecond = 0.0f;
|
||||
|
||||
@Override
|
||||
public void setSamplesPerMillisecond(float samplesPerMillisecond)
|
||||
{
|
||||
mSamplesPerMillisecond = samplesPerMillisecond;
|
||||
}
|
||||
|
||||
/**
|
||||
* Quantity of I/Q sample pairs per milli-second at the current sample rate to use in calculating an accurate
|
||||
* timestamp for sub-buffer that are generated from the native buffer.
|
||||
* @return samples per millisecond.
|
||||
*/
|
||||
public float getSamplesPerMillisecond()
|
||||
{
|
||||
return mSamplesPerMillisecond;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import java.util.Iterator;
|
||||
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
|
||||
* non-interleaved complex sample buffers converted from the raw byte sample array.
|
||||
*/
|
||||
public class ByteNativeBuffer implements INativeBuffer
|
||||
public class ByteNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
private static final int FRAGMENT_SIZE = 2048;
|
||||
private final static float[] LOOKUP_VALUES;
|
||||
@@ -46,16 +46,17 @@ public class ByteNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
private byte[] mSamples;
|
||||
private long mTimestamp;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param samples to process
|
||||
* @param timestamp of the samples
|
||||
* @param averageDc measured from sample stream
|
||||
* @param samplesPerMillisecond to calculate derivative timestamps for sub-buffers.
|
||||
*/
|
||||
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc)
|
||||
public ByteNativeBuffer(byte[] samples, long timestamp, float averageDc, float samplesPerMillisecond)
|
||||
{
|
||||
super(timestamp, samplesPerMillisecond);
|
||||
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
|
||||
if(samples.length % FRAGMENT_SIZE != 0)
|
||||
{
|
||||
@@ -63,7 +64,6 @@ public class ByteNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
mSamples = samples;
|
||||
mTimestamp = timestamp;
|
||||
mAverageDc = averageDc;
|
||||
}
|
||||
|
||||
@@ -73,12 +73,6 @@ public class ByteNativeBuffer implements INativeBuffer
|
||||
return mSamples.length / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ComplexSamples> iterator()
|
||||
{
|
||||
@@ -104,6 +98,8 @@ public class ByteNativeBuffer implements INativeBuffer
|
||||
@Override
|
||||
public ComplexSamples next()
|
||||
{
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
|
||||
float[] i = new float[FRAGMENT_SIZE];
|
||||
float[] q = new float[FRAGMENT_SIZE];
|
||||
int samplesOffset = mSamplesPointer;
|
||||
@@ -115,8 +111,7 @@ public class ByteNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
mSamplesPointer = samplesOffset;
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +131,8 @@ public class ByteNativeBuffer implements INativeBuffer
|
||||
@Override
|
||||
public InterleavedComplexSamples next()
|
||||
{
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
|
||||
float[] converted = new float[FRAGMENT_SIZE * 2];
|
||||
|
||||
int samplesPointer = mSamplesPointer;
|
||||
@@ -147,7 +144,7 @@ public class ByteNativeBuffer implements INativeBuffer
|
||||
|
||||
mSamplesPointer = samplesPointer;
|
||||
|
||||
return new InterleavedComplexSamples(converted, mTimestamp);
|
||||
return new InterleavedComplexSamples(converted, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.nio.ByteBuffer;
|
||||
/**
|
||||
* Implements a factory for creating ByteNativeBuffer instances
|
||||
*/
|
||||
public class ByteNativeBufferFactory implements INativeBufferFactory
|
||||
public class ByteNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
{
|
||||
private DcCorrectionManager mDcCorrectionManager = new DcCorrectionManager();
|
||||
|
||||
@@ -39,7 +39,7 @@ public class ByteNativeBufferFactory implements INativeBufferFactory
|
||||
calculateDc(copy);
|
||||
}
|
||||
|
||||
return new ByteNativeBuffer(copy, timestamp, mDcCorrectionManager.getAverageDc());
|
||||
return new ByteNativeBuffer(copy, timestamp, mDcCorrectionManager.getAverageDc(), getSamplesPerMillisecond());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,35 +22,24 @@ package io.github.dsheirer.buffer;
|
||||
import io.github.dsheirer.sample.SampleUtils;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Simple native buffer implementation that simply wraps a single, existing, non-native sample buffer.
|
||||
*/
|
||||
public class FloatNativeBuffer implements INativeBuffer
|
||||
public class FloatNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
private float[] mInterleavedComplexSamples;
|
||||
private long mTimestamp;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param complexSamples to wrap
|
||||
* @param complexSamples interleaved to wrap
|
||||
* @param timestamp for the buffer
|
||||
*/
|
||||
public FloatNativeBuffer(float[] complexSamples, long timestamp)
|
||||
public FloatNativeBuffer(float[] complexSamples, long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(timestamp, samplesPerMillisecond);
|
||||
mInterleavedComplexSamples = complexSamples;
|
||||
mTimestamp = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
* @param complexSamples to wrap
|
||||
*/
|
||||
public FloatNativeBuffer(float[] complexSamples)
|
||||
{
|
||||
this(complexSamples, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,9 +47,9 @@ public class FloatNativeBuffer implements INativeBuffer
|
||||
* @param samples to wrap
|
||||
* @param timestamp for the buffer
|
||||
*/
|
||||
public FloatNativeBuffer(ComplexSamples samples, long timestamp)
|
||||
public FloatNativeBuffer(ComplexSamples samples)
|
||||
{
|
||||
this(SampleUtils.interleave(samples), timestamp);
|
||||
this(SampleUtils.interleave(samples), samples.timestamp(), 0.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,7 +58,7 @@ public class FloatNativeBuffer implements INativeBuffer
|
||||
*/
|
||||
public FloatNativeBuffer(InterleavedComplexSamples samples)
|
||||
{
|
||||
this(samples.samples(), samples.timestamp());
|
||||
this(samples.samples(), samples.timestamp(), 0.0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -90,12 +79,6 @@ public class FloatNativeBuffer implements INativeBuffer
|
||||
return mInterleavedComplexSamples.length / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
private class ComplexSamplesIterator implements Iterator<ComplexSamples>
|
||||
{
|
||||
private boolean mEmpty;
|
||||
@@ -115,7 +98,7 @@ public class FloatNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
mEmpty = true;
|
||||
return SampleUtils.deinterleave(mInterleavedComplexSamples);
|
||||
return SampleUtils.deinterleave(mInterleavedComplexSamples, getTimestamp());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +122,7 @@ public class FloatNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
mEmpty = true;
|
||||
return new InterleavedComplexSamples(mInterleavedComplexSamples, mTimestamp);
|
||||
return new InterleavedComplexSamples(mInterleavedComplexSamples, getTimestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,15 @@ public interface INativeBufferFactory
|
||||
*
|
||||
* @param samples byte array copied from native memory
|
||||
* @param timestamp of the samples
|
||||
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
|
||||
* @return instance
|
||||
*/
|
||||
INativeBuffer getBuffer(ByteBuffer samples, long timestamp);
|
||||
|
||||
/**
|
||||
* Sets the samples per millisecond rate based on the current sample rate.
|
||||
*
|
||||
* @param samplesPerMillisecond to calculate timestamp offset for child buffers.
|
||||
*/
|
||||
void setSamplesPerMillisecond(float samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ package io.github.dsheirer.buffer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Native buffer sample array wrapper class that provides access to a stream of either interleaved or
|
||||
* non-interleaved complex sample buffers converted from the raw byte sample array.
|
||||
*/
|
||||
public class SignedByteNativeBuffer implements INativeBuffer
|
||||
public class SignedByteNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
private static final int FRAGMENT_SIZE = 2048;
|
||||
private final static float[] LOOKUP_VALUES;
|
||||
@@ -47,7 +46,6 @@ public class SignedByteNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
private byte[] mSamples;
|
||||
private long mTimestamp;
|
||||
private float mIAverageDc;
|
||||
private float mQAverageDc;
|
||||
|
||||
@@ -57,9 +55,12 @@ public class SignedByteNativeBuffer implements INativeBuffer
|
||||
* @param timestamp of the samples
|
||||
* @param iAverageDc of the sample stream
|
||||
* @param qAverageDc of the sample stream
|
||||
* @param samplesPerMillisecond to calculate sub-buffer timestamps
|
||||
*/
|
||||
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc)
|
||||
public SignedByteNativeBuffer(byte[] samples, long timestamp, float iAverageDc, float qAverageDc, float samplesPerMillisecond)
|
||||
{
|
||||
super(timestamp, samplesPerMillisecond);
|
||||
|
||||
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
|
||||
if(samples.length % FRAGMENT_SIZE != 0)
|
||||
{
|
||||
@@ -67,17 +68,10 @@ public class SignedByteNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
mSamples = samples;
|
||||
mTimestamp = timestamp;
|
||||
mIAverageDc = iAverageDc;
|
||||
mQAverageDc = qAverageDc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int sampleCount()
|
||||
{
|
||||
@@ -112,6 +106,8 @@ public class SignedByteNativeBuffer implements INativeBuffer
|
||||
@Override
|
||||
public ComplexSamples next()
|
||||
{
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
|
||||
float[] i = new float[FRAGMENT_SIZE];
|
||||
float[] q = new float[FRAGMENT_SIZE];
|
||||
int samplesOffset = mSamplesPointer;
|
||||
@@ -123,7 +119,7 @@ public class SignedByteNativeBuffer implements INativeBuffer
|
||||
}
|
||||
|
||||
mSamplesPointer = samplesOffset;
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +139,8 @@ public class SignedByteNativeBuffer implements INativeBuffer
|
||||
@Override
|
||||
public InterleavedComplexSamples next()
|
||||
{
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
|
||||
float[] converted = new float[FRAGMENT_SIZE * 2];
|
||||
|
||||
int samplesOffset = mSamplesPointer;
|
||||
@@ -155,7 +153,7 @@ public class SignedByteNativeBuffer implements INativeBuffer
|
||||
|
||||
mSamplesPointer = samplesOffset;
|
||||
|
||||
return new InterleavedComplexSamples(converted, mTimestamp);
|
||||
return new InterleavedComplexSamples(converted, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.util.concurrent.TimeUnit;
|
||||
/**
|
||||
* Implements a factory for creating SignedByteNativeBuffer instances
|
||||
*/
|
||||
public class SignedByteNativeBufferFactory implements INativeBufferFactory
|
||||
public class SignedByteNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
{
|
||||
/**
|
||||
* DC removal calculations will run once a minute
|
||||
@@ -78,7 +78,7 @@ public class SignedByteNativeBufferFactory implements INativeBufferFactory
|
||||
calculateDc(copy);
|
||||
}
|
||||
|
||||
return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc);
|
||||
return new SignedByteNativeBuffer(copy, timestamp, mIAverageDc, mQAverageDc, getSamplesPerMillisecond());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
@@ -42,8 +41,9 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
|
||||
protected float[] mQBuffer = new float[FRAGMENT_SIZE + Q_OVERLAP];
|
||||
protected short[] mSamples;
|
||||
protected int mSamplesPointer = 0;
|
||||
protected long mTimestamp;
|
||||
protected float mAverageDc;
|
||||
private long mTimestamp;
|
||||
private float mSamplesPerMillisecond;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -52,8 +52,10 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
|
||||
* @param residualQ samples from previous buffer
|
||||
* @param averageDc as measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
|
||||
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
|
||||
float samplesPerMillisecond)
|
||||
{
|
||||
if(residualI.length != I_OVERLAP || residualQ.length != Q_OVERLAP)
|
||||
{
|
||||
@@ -73,6 +75,7 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
|
||||
mAverageDc = averageDc;
|
||||
mSamples = samples;
|
||||
mTimestamp = timestamp;
|
||||
mSamplesPerMillisecond = samplesPerMillisecond;
|
||||
|
||||
//Transfer and scale the residual I & Q samples from the previous buffer
|
||||
for(int i = 0; i < residualI.length; i++)
|
||||
@@ -86,6 +89,17 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the timestamp for a samples buffer fragment based on where it is extracted from relative to the
|
||||
* native buffer starting timestamp.
|
||||
* @param samplesPointer for the start of the fragment. Note: this value will be divided by 2 to account for I/Q sample pairs.
|
||||
* @return timestamp adjusted to the fragment or sub-buffer start sample.
|
||||
*/
|
||||
protected long getFragmentTimestamp(int samplesPointer)
|
||||
{
|
||||
return mTimestamp + (long)(samplesPointer / 2 / mSamplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@@ -36,10 +35,11 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
|
||||
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,6 +50,8 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
|
||||
int offset = mSamplesPointer;
|
||||
|
||||
for(int x = 0; x < FRAGMENT_SIZE; x++)
|
||||
@@ -91,6 +93,6 @@ public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamp
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,11 @@
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
*/
|
||||
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
|
||||
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,6 +56,7 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -121,6 +123,6 @@ public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<Comp
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,11 @@
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
*/
|
||||
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
|
||||
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,6 +56,7 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -115,6 +117,6 @@ public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<Comp
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,11 @@
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
*/
|
||||
@@ -42,10 +41,12 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
|
||||
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
|
||||
mFilterPart2 = new float[16];
|
||||
System.arraycopy(COEFFICIENTS, 16, mFilterPart2, 8, 8);
|
||||
@@ -59,6 +60,8 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -117,6 +120,6 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,11 @@
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Vector SIMD implementation for non-packed Airspy native buffers
|
||||
*/
|
||||
@@ -41,10 +40,12 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp)
|
||||
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,6 +56,8 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -133,6 +136,6 @@ public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<Compl
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,11 +34,12 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp)
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -49,6 +50,7 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
int offset = mSamplesPointer;
|
||||
|
||||
for(int x = 0; x < FRAGMENT_SIZE; x++)
|
||||
@@ -89,6 +91,6 @@ public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new InterleavedComplexSamples(samples, mTimestamp);
|
||||
return new InterleavedComplexSamples(samples, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp)
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -118,6 +120,6 @@ public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIt
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new InterleavedComplexSamples(samples, mTimestamp);
|
||||
return new InterleavedComplexSamples(samples, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp)
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -112,6 +114,6 @@ public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIt
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new InterleavedComplexSamples(samples, mTimestamp);
|
||||
return new InterleavedComplexSamples(samples, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,11 +40,12 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp)
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
|
||||
//Arrange the last 8 taps of the filter into the higher indices of an array to align for SIMD
|
||||
mFilterPart2 = new float[16];
|
||||
@@ -59,6 +60,7 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -115,6 +117,6 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new InterleavedComplexSamples(samples, mTimestamp);
|
||||
return new InterleavedComplexSamples(samples, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,11 +39,12 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
|
||||
* @param residualQ samples from last buffer
|
||||
* @param averageDc measured
|
||||
* @param timestamp of the buffer
|
||||
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
|
||||
*/
|
||||
public AirspyInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp)
|
||||
long timestamp, float samplesPerMillisecond)
|
||||
{
|
||||
super(samples, residualI, residualQ, averageDc, timestamp);
|
||||
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,6 +55,7 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
|
||||
throw new IllegalStateException("End of buffer exceeded");
|
||||
}
|
||||
|
||||
long timestamp = getFragmentTimestamp(mSamplesPointer);
|
||||
int offset = mSamplesPointer;
|
||||
int fragmentPointer = 0;
|
||||
float[] scaledSamples = new float[VECTOR_SPECIES.length()];
|
||||
@@ -130,6 +132,6 @@ public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIte
|
||||
System.arraycopy(mIBuffer, FRAGMENT_SIZE, mIBuffer, 0, I_OVERLAP);
|
||||
System.arraycopy(mQBuffer, FRAGMENT_SIZE, mQBuffer, 0, Q_OVERLAP);
|
||||
|
||||
return new InterleavedComplexSamples(samples, mTimestamp);
|
||||
return new InterleavedComplexSamples(samples, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,23 +19,21 @@
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.buffer.INativeBuffer;
|
||||
import io.github.dsheirer.buffer.AbstractNativeBuffer;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Native buffer scalar implementation for Airspy non-packed samples.
|
||||
*/
|
||||
public class AirspyNativeBuffer implements INativeBuffer
|
||||
public class AirspyNativeBuffer extends AbstractNativeBuffer
|
||||
{
|
||||
private short[] mSamples;
|
||||
private short[] mResidualI;
|
||||
private short[] mResidualQ;
|
||||
private float mAverageDc;
|
||||
private long mTimestamp;
|
||||
private Implementation mInterleavedImplementation;
|
||||
private Implementation mNonInterleavedImplementation;
|
||||
|
||||
@@ -48,11 +46,14 @@ public class AirspyNativeBuffer implements INativeBuffer
|
||||
* @param timestamp of the buffer
|
||||
* @param interleavedImplementation optimal, scalar vs vector SIMD
|
||||
* @param nonInterleavedImplementation optimal, scalar vs vector SIMD
|
||||
* @param samplesPerMillisecond used to calculate sub-buffer fragment timestamp offsets from the start of this buffer.
|
||||
*/
|
||||
public AirspyNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
|
||||
long timestamp, Implementation interleavedImplementation,
|
||||
Implementation nonInterleavedImplementation)
|
||||
Implementation nonInterleavedImplementation, float samplesPerMillisecond)
|
||||
{
|
||||
super(timestamp, samplesPerMillisecond);
|
||||
|
||||
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
|
||||
if(samples.length % AirspyBufferIterator.FRAGMENT_SIZE != 0)
|
||||
{
|
||||
@@ -64,7 +65,6 @@ public class AirspyNativeBuffer implements INativeBuffer
|
||||
mResidualI = residualI;
|
||||
mResidualQ = residualQ;
|
||||
mAverageDc = averageDc;
|
||||
mTimestamp = timestamp;
|
||||
mInterleavedImplementation = interleavedImplementation;
|
||||
mNonInterleavedImplementation = nonInterleavedImplementation;
|
||||
}
|
||||
@@ -75,22 +75,16 @@ public class AirspyNativeBuffer implements INativeBuffer
|
||||
return mSamples.length / 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<ComplexSamples> iterator()
|
||||
{
|
||||
return switch(mInterleavedImplementation)
|
||||
{
|
||||
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,11 +93,11 @@ public class AirspyNativeBuffer implements INativeBuffer
|
||||
{
|
||||
return switch(mInterleavedImplementation)
|
||||
{
|
||||
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, mTimestamp);
|
||||
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,18 @@
|
||||
|
||||
package io.github.dsheirer.buffer.airspy;
|
||||
|
||||
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
|
||||
import io.github.dsheirer.buffer.INativeBuffer;
|
||||
import io.github.dsheirer.buffer.INativeBufferFactory;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationManager;
|
||||
import io.github.dsheirer.vector.calibrate.CalibrationType;
|
||||
import io.github.dsheirer.vector.calibrate.Implementation;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Implements a factory for creating SignedByteNativeBuffer instances
|
||||
*/
|
||||
public class AirspyNativeBufferFactory implements INativeBufferFactory
|
||||
public class AirspyNativeBufferFactory extends AbstractNativeBufferFactory
|
||||
{
|
||||
private boolean mSamplePacking = false;
|
||||
private short[] mResidualI = new short[AirspyBufferIterator.I_OVERLAP];
|
||||
@@ -106,7 +105,7 @@ public class AirspyNativeBufferFactory implements INativeBufferFactory
|
||||
INativeBuffer nativeBuffer = new AirspyNativeBuffer(samples,
|
||||
Arrays.copyOf(mResidualI, mResidualI.length),
|
||||
Arrays.copyOf(mResidualQ, mResidualQ.length), mConverter.getAverageDc(), timestamp,
|
||||
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation);
|
||||
mInterleavedIteratorImplementation, mNonInterleavedIteratorImplementation, getSamplesPerMillisecond());
|
||||
|
||||
extractResidual(samples);
|
||||
|
||||
|
||||
@@ -23,11 +23,10 @@ import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public abstract class AbstractComplexPolyphaseChannelizer implements Listener<InterleavedComplexSamples>, ISourceEventListener
|
||||
{
|
||||
@@ -38,6 +37,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
|
||||
private int mChannelCount;
|
||||
private int mSubChannelCount;
|
||||
private double mChannelSampleRate;
|
||||
protected long mCurrentSamplesTimestamp;
|
||||
|
||||
/**
|
||||
* Complex sample polyphase channelizer
|
||||
@@ -106,7 +106,7 @@ public abstract class AbstractComplexPolyphaseChannelizer implements Listener<In
|
||||
{
|
||||
for(PolyphaseChannelSource channel : mChannels)
|
||||
{
|
||||
channel.receiveChannelResults(channelResultsList);
|
||||
channel.receiveChannelResults(channelResultsList, mCurrentSamplesTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,14 @@ import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
|
||||
import io.github.dsheirer.util.Dispatcher;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.jtransforms.fft.FloatFFT_1D;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample
|
||||
@@ -193,7 +192,8 @@ public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChann
|
||||
@Override
|
||||
public void receive(InterleavedComplexSamples complexSamples)
|
||||
{
|
||||
//TODO: how do we pass the timestamp onto the sample assemblers?
|
||||
mCurrentSamplesTimestamp = complexSamples.timestamp();
|
||||
|
||||
float[] samples = complexSamples.samples();
|
||||
|
||||
int samplesPointer = 0;
|
||||
|
||||
@@ -22,10 +22,6 @@ import io.github.dsheirer.buffer.INativeBuffer;
|
||||
import io.github.dsheirer.buffer.INativeBufferProvider;
|
||||
import io.github.dsheirer.buffer.NativeBufferPoisonPill;
|
||||
import io.github.dsheirer.controller.channel.event.ChannelStopProcessingRequest;
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.eventbus.MyEventBus;
|
||||
import io.github.dsheirer.sample.Broadcaster;
|
||||
@@ -44,10 +40,8 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
@@ -75,12 +69,12 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
private static final double MINIMUM_CHANNEL_BANDWIDTH = 25000.0;
|
||||
private static final double CHANNEL_OVERSAMPLING = 2.0;
|
||||
private static final int POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL = 9;
|
||||
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
|
||||
|
||||
private Broadcaster<SourceEvent> mSourceEventBroadcaster = new Broadcaster<>();
|
||||
private INativeBufferProvider mNativeBufferProvider;
|
||||
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
|
||||
private ChannelCalculator mChannelCalculator;
|
||||
private SynthesisFilterManager mFilterManager = new SynthesisFilterManager();
|
||||
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
|
||||
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
|
||||
private NativeBufferReceiver mNativeBufferReceiver = new NativeBufferReceiver();
|
||||
@@ -181,63 +175,22 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
|
||||
if(mRunning)
|
||||
{
|
||||
List<Integer> polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);
|
||||
|
||||
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
|
||||
|
||||
if(outputProcessor != null)
|
||||
try
|
||||
{
|
||||
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, mChannelCalculator, mFilterManager,
|
||||
mChannelSourceEventListener);
|
||||
|
||||
try
|
||||
{
|
||||
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
|
||||
mChannelCalculator.getChannelSampleRate(), centerFrequency);
|
||||
|
||||
mChannelSources.add(channelSource);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
|
||||
}
|
||||
mChannelSources.add(channelSource);
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.debug("Couldn't design final output low pass filter for polyphase channel source");
|
||||
}
|
||||
}
|
||||
|
||||
return channelSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a processor to process the channelizer channel indexes into a composite output stream providing
|
||||
* channelized complex sample buffers to a registered source listener.
|
||||
* @param indexes to target by the output processor
|
||||
* @return output processor compatible with the number of indexes to monitor
|
||||
*/
|
||||
private IPolyphaseChannelOutputProcessor getOutputProcessor(List<Integer> indexes)
|
||||
{
|
||||
switch(indexes.size())
|
||||
{
|
||||
case 1:
|
||||
return new OneChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes,
|
||||
mChannelCalculator.getChannelCount());
|
||||
case 2:
|
||||
try
|
||||
{
|
||||
float[] filter = getOutputProcessorFilter(2);
|
||||
return new TwoChannelOutputProcessor(mChannelCalculator.getChannelSampleRate(), indexes, filter,
|
||||
mChannelCalculator.getChannelCount());
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error designing 2 channel synthesis filter for output processor");
|
||||
}
|
||||
default:
|
||||
//TODO: create output processor for greater than 2 input channels
|
||||
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
|
||||
mLog.info(mChannelCalculator.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts/adds the channel source to receive channelized sample buffers, registering with the tuner to receive
|
||||
* sample buffers when this is the first channel.
|
||||
@@ -249,7 +202,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
synchronized(mBufferDispatcher)
|
||||
{
|
||||
//Note: the polyphase channel source has already been added to the mChannelSources in getChannel() method
|
||||
|
||||
checkChannelizerConfiguration();
|
||||
|
||||
mPolyphaseChannelizer.addChannel(channelSource);
|
||||
@@ -311,10 +263,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
switch(sourceEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
//Update channel calculator immediately so that channels can be allocated
|
||||
mChannelCalculator.setCenterFrequency(sourceEvent.getValue().longValue());
|
||||
|
||||
//Defer channelizer configuration changes to be handled on the buffer processor thread
|
||||
mNativeBufferReceiver.receive(sourceEvent);
|
||||
break;
|
||||
case NOTIFICATION_SAMPLE_RATE_CHANGE:
|
||||
@@ -381,79 +329,21 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
* Updates each of the output processors for any changes in the tuner's center frequency or sample rate, which
|
||||
* would cause the output processors to change the polyphase channelizer results channel(s) that the processor is
|
||||
* consuming
|
||||
*
|
||||
* @param sourceEvent (optional-can be null) to broadcast to each output processor following the update
|
||||
*/
|
||||
private void updateOutputProcessors(SourceEvent sourceEvent)
|
||||
private void updateOutputProcessors()
|
||||
{
|
||||
for(PolyphaseChannelSource channelSource: mChannelSources)
|
||||
{
|
||||
updateOutputProcessor(channelSource);
|
||||
|
||||
//Send the non-null source event to each channel source
|
||||
if(sourceEvent != null)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
channelSource.process(sourceEvent);
|
||||
}
|
||||
catch(SourceException se)
|
||||
{
|
||||
mLog.error("Error while notifying polyphase channel source of a source event", se);
|
||||
}
|
||||
channelSource.updateOutputProcessor(mChannelCalculator, mFilterManager);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the polyphase channel source's output processor due to a change in the center frequency or sample
|
||||
* rate for the source providing sample buffers to the polyphase channelizer, or whenever the DDC channel's
|
||||
* center tuned frequency changes.
|
||||
*
|
||||
* @param channelSource that requires an update to its output processor
|
||||
*/
|
||||
private void updateOutputProcessor(PolyphaseChannelSource channelSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
|
||||
//calculator will throw an IllegalArgException ... handled below
|
||||
List<Integer> indexes = mChannelCalculator.getChannelIndexes(channelSource.getTunerChannel());
|
||||
|
||||
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(indexes);
|
||||
|
||||
//If the indexes size is the same then update the current processor, otherwise create a new one
|
||||
IPolyphaseChannelOutputProcessor outputProcessor = channelSource.getPolyphaseChannelOutputProcessor();
|
||||
|
||||
if(outputProcessor != null && outputProcessor.getInputChannelCount() == indexes.size())
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
channelSource.getPolyphaseChannelOutputProcessor().setPolyphaseChannelIndices(indexes);
|
||||
channelSource.setFrequency(centerFrequency);
|
||||
|
||||
if(indexes.size() > 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
float[] filter = getOutputProcessorFilter(indexes.size());
|
||||
channelSource.getPolyphaseChannelOutputProcessor().setSynthesisFilter(filter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error creating an updated synthesis filter for the channel output processor");
|
||||
}
|
||||
}
|
||||
mLog.error("Error updating polyphase channel source output processor following tuner frequency or " +
|
||||
"sample rate change");
|
||||
stopChannelSource(channelSource);
|
||||
}
|
||||
else
|
||||
{
|
||||
channelSource.setPolyphaseChannelOutputProcessor(getOutputProcessor(indexes), centerFrequency);
|
||||
}
|
||||
}
|
||||
catch(IllegalArgumentException iae)
|
||||
{
|
||||
mLog.error("Error updating polyphase channel source - can't determine output channel indexes for " +
|
||||
"updated tuner center frequency and sample rate. Stopping channel source", iae);
|
||||
|
||||
stopChannelSource(channelSource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,29 +387,6 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
mSourceEventBroadcaster.removeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates (or reuses) an output processor filter for the specified number of channels. Each
|
||||
* filter is created only once and stored in a map for reuse. This map is cleared anytime that the
|
||||
* input sample rate changes, so that the filters can be recreated with the new channel sample rate.
|
||||
* @param channels count
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed to specification (-6 dB band edge)
|
||||
*/
|
||||
private float[] getOutputProcessorFilter(int channels) throws FilterDesignException
|
||||
{
|
||||
float[] taps = mOutputProcessorFilters.get(channels);
|
||||
|
||||
if(taps == null)
|
||||
{
|
||||
taps = FilterFactory.getSincM2Synthesizer(mChannelCalculator.getChannelSampleRate(),
|
||||
mChannelCalculator.getChannelBandwidth(), channels, POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
|
||||
|
||||
mOutputProcessorFilters.put(channels, taps);
|
||||
}
|
||||
|
||||
return taps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal class for handling requests for start/stop sample stream from polyphase channel sources
|
||||
*/
|
||||
@@ -584,7 +451,7 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
*/
|
||||
public class NativeBufferReceiver implements Listener<INativeBuffer>
|
||||
{
|
||||
private Queue<SourceEvent> mQueuedSourceEvents = new ConcurrentLinkedQueue<>();
|
||||
private boolean mOutputProcessorUpdateRequired = false;
|
||||
|
||||
/**
|
||||
* Queues the source event for deferred execution on the buffer processing thread.
|
||||
@@ -592,7 +459,13 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
*/
|
||||
public void receive(SourceEvent event)
|
||||
{
|
||||
mQueuedSourceEvents.offer(event);
|
||||
long frequency = event.getValue().longValue();
|
||||
|
||||
if(mChannelCalculator.getCenterFrequency() != frequency)
|
||||
{
|
||||
mChannelCalculator.setCenterFrequency(frequency);
|
||||
mOutputProcessorUpdateRequired = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -600,20 +473,10 @@ public class PolyphaseChannelManager implements ISourceEventProcessor
|
||||
{
|
||||
try
|
||||
{
|
||||
//Process any queued source events before processing the buffers
|
||||
SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll();
|
||||
|
||||
while(queuedSourceEvent != null)
|
||||
if(mOutputProcessorUpdateRequired)
|
||||
{
|
||||
switch(queuedSourceEvent.getEvent())
|
||||
{
|
||||
case NOTIFICATION_FREQUENCY_CHANGE:
|
||||
//Don't send the tuner's frequency change event down to the channels - it would cause chaos
|
||||
updateOutputProcessors(null);
|
||||
break;
|
||||
}
|
||||
|
||||
queuedSourceEvent = mQueuedSourceEvents.poll();
|
||||
updateOutputProcessors();
|
||||
mOutputProcessorUpdateRequired = false;
|
||||
}
|
||||
|
||||
if(mPolyphaseChannelizer != null)
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.OneChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.channelizer.output.TwoChannelOutputProcessor;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -26,8 +28,10 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.source.tuner.channel.StreamProcessorWithHeartbeat;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannel;
|
||||
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Polyphase Channelizer's Tuner Channel Source implementation. Wraps a ChannelOutputProcessor instance and
|
||||
@@ -36,34 +40,30 @@ import java.util.List;
|
||||
*/
|
||||
public class PolyphaseChannelSource extends TunerChannelSource implements Listener<ComplexSamples>
|
||||
{
|
||||
private Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
|
||||
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
|
||||
private IPolyphaseChannelOutputProcessor mReplacementPolyphaseChannelOutputProcessor;
|
||||
private StreamProcessorWithHeartbeat<ComplexSamples> mStreamHeartbeatProcessor;
|
||||
private long mReplacementFrequency;
|
||||
private double mChannelSampleRate;
|
||||
private long mIndexCenterFrequency;
|
||||
private long mChannelFrequencyCorrection;
|
||||
private ReentrantLock mOutputProcessorLock = new ReentrantLock();
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
*
|
||||
* @param tunerChannel describing the desired channel frequency and bandwidth/minimum sample rate
|
||||
* @param outputProcessor - to process polyphase channelizer channel results into a channel stream
|
||||
* @param channelCalculator for current channel center frequency and sample rate and index calculations
|
||||
* @param filterManager for access to new or cached synthesis filters
|
||||
* @param producerSourceEventListener to receive source event requests (e.g. start/stop sample stream)
|
||||
* @param channelSampleRate for the downstream sample output
|
||||
* @param centerFrequency of the incoming polyphase channel(s)
|
||||
* @throws FilterDesignException if a channel low pass filter can't be designed to the channel specification
|
||||
* @throws IllegalArgumentException if a channel low pass filter can't be designed to the channel specification
|
||||
*/
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
|
||||
Listener<SourceEvent> producerSourceEventListener, double channelSampleRate,
|
||||
long centerFrequency) throws FilterDesignException
|
||||
public PolyphaseChannelSource(TunerChannel tunerChannel, ChannelCalculator channelCalculator, SynthesisFilterManager filterManager,
|
||||
Listener<SourceEvent> producerSourceEventListener) throws IllegalArgumentException
|
||||
{
|
||||
super(producerSourceEventListener, tunerChannel);
|
||||
mPolyphaseChannelOutputProcessor = outputProcessor;
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mChannelSampleRate = channelSampleRate;
|
||||
mChannelSampleRate = channelCalculator.getChannelSampleRate();
|
||||
mStreamHeartbeatProcessor = new StreamProcessorWithHeartbeat<>(getHeartbeatManager(), HEARTBEAT_INTERVAL_MS);
|
||||
setFrequency(centerFrequency);
|
||||
updateOutputProcessor(channelCalculator, filterManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,58 +109,101 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel output processor used by this channel source to convert polyphase channel results into a specific
|
||||
* channel complex buffer output stream.
|
||||
* Updates the output processor whenever the source tuner's center frequency changes.
|
||||
* @param channelCalculator providing access to updated tuner center frequency, sample rate, etc.
|
||||
* @param filterManager for designing and caching synthesis filters
|
||||
*/
|
||||
public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor()
|
||||
public void updateOutputProcessor(ChannelCalculator channelCalculator, SynthesisFilterManager filterManager) throws IllegalArgumentException
|
||||
{
|
||||
return mPolyphaseChannelOutputProcessor;
|
||||
}
|
||||
String errorMessage = null;
|
||||
|
||||
/**
|
||||
* Sets/updates the output processor for this channel source, replacing the existing output processor.
|
||||
*
|
||||
* @param outputProcessor to use
|
||||
* @param frequency center for the channels processed by the output processor
|
||||
*/
|
||||
public void setPolyphaseChannelOutputProcessor(IPolyphaseChannelOutputProcessor outputProcessor, long frequency)
|
||||
{
|
||||
//If this is the first time, simply assign the output processor
|
||||
if(mPolyphaseChannelOutputProcessor == null)
|
||||
mOutputProcessorLock.lock();
|
||||
|
||||
try
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor = outputProcessor;
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
//If a change in sample rate or center frequency makes this channel no longer viable, then the channel
|
||||
//calculator will throw an IllegalArgException ... handled below
|
||||
List<Integer> indexes = channelCalculator.getChannelIndexes(getTunerChannel());
|
||||
|
||||
//The provided channels are necessarily aligned to the center frequency that this source is providing and an
|
||||
//oscillator will mix the provided channels to bring the desired center frequency to baseband.
|
||||
setFrequency(channelCalculator.getCenterFrequencyForIndexes(indexes));
|
||||
|
||||
if(mPolyphaseChannelOutputProcessor != null && mPolyphaseChannelOutputProcessor.getInputChannelCount() == indexes.size())
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.setPolyphaseChannelIndices(indexes);
|
||||
|
||||
if(indexes.size() > 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
float[] filter = filterManager.getFilter(channelCalculator.getSampleRate(),
|
||||
channelCalculator.getChannelBandwidth(), indexes.size());
|
||||
mPolyphaseChannelOutputProcessor.setSynthesisFilter(filter);
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
mLog.error("Error creating an updated synthesis filter for the channel output processor");
|
||||
errorMessage ="Cannot update output processor - unable to design synthesis filter for [" +
|
||||
indexes.size() + "] channel indices";
|
||||
}
|
||||
}
|
||||
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
}
|
||||
else //Create a new output processor.
|
||||
{
|
||||
if(mPolyphaseChannelOutputProcessor != null)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.setListener(null);
|
||||
}
|
||||
|
||||
mPolyphaseChannelOutputProcessor = null;
|
||||
|
||||
switch(indexes.size())
|
||||
{
|
||||
case 1:
|
||||
mPolyphaseChannelOutputProcessor = new OneChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
|
||||
indexes, channelCalculator.getChannelCount());
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
mPolyphaseChannelOutputProcessor.start();
|
||||
break;
|
||||
case 2:
|
||||
try
|
||||
{
|
||||
float[] filter = filterManager.getFilter(channelCalculator.getChannelSampleRate(),
|
||||
channelCalculator.getChannelBandwidth(), 2);
|
||||
mPolyphaseChannelOutputProcessor = new TwoChannelOutputProcessor(channelCalculator.getChannelSampleRate(),
|
||||
indexes, filter, channelCalculator.getChannelCount());
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
mPolyphaseChannelOutputProcessor.start();
|
||||
}
|
||||
catch(FilterDesignException fde)
|
||||
{
|
||||
errorMessage = "Cannot create new output processor - unable to design synthesis filter for [" +
|
||||
indexes.size() + "] channel indices";
|
||||
mLog.error("Error creating a synthesis filter for a new channel output processor");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
mLog.error("Request to create an output processor for unexpected channel index size:" + indexes.size());
|
||||
mLog.info(channelCalculator.toString());
|
||||
errorMessage = "Unable to create new channel output processor - unexpected channel index size: " +
|
||||
indexes.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
//Otherwise, we have to swap out the processor on the sample processing thread
|
||||
else
|
||||
finally
|
||||
{
|
||||
mReplacementPolyphaseChannelOutputProcessor = outputProcessor;
|
||||
mReplacementFrequency = frequency;
|
||||
mOutputProcessorLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the output processor to use the new output processor provided by the external
|
||||
* polyphase channel manager. This method should only be executed on the sample processing
|
||||
* thread, within the processChannelResults() method.
|
||||
*/
|
||||
private void swapOutputProcessor()
|
||||
{
|
||||
if(mReplacementPolyphaseChannelOutputProcessor != null)
|
||||
//Unlikely, but if we had an error designing a synthesis filter, throw an exception
|
||||
if(errorMessage != null)
|
||||
{
|
||||
IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor;
|
||||
existingProcessor.stop();
|
||||
existingProcessor.setListener(null);
|
||||
|
||||
//Swap out the processor so that incoming samples can accumulate in the new channel output processor
|
||||
mPolyphaseChannelOutputProcessor = mReplacementPolyphaseChannelOutputProcessor;
|
||||
mReplacementPolyphaseChannelOutputProcessor = null;
|
||||
mPolyphaseChannelOutputProcessor.setListener(this);
|
||||
mPolyphaseChannelOutputProcessor.start();
|
||||
|
||||
//Finally, setup the frequency offset for the output processor.
|
||||
mIndexCenterFrequency = mReplacementFrequency;
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
throw new IllegalArgumentException(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,15 +213,23 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
* channel aggregation, and dispatch the results to the downstream sample listener/consumer.
|
||||
*
|
||||
* @param channelResultsList containing a list of polyphase channelizer output arrays.
|
||||
* @param currentSamplesTimestamp for the samples
|
||||
*/
|
||||
public void receiveChannelResults(List<float[]> channelResultsList)
|
||||
public void receiveChannelResults(List<float[]> channelResultsList, long currentSamplesTimestamp)
|
||||
{
|
||||
if(mReplacementPolyphaseChannelOutputProcessor != null)
|
||||
{
|
||||
swapOutputProcessor();
|
||||
}
|
||||
mOutputProcessorLock.lock();
|
||||
|
||||
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList);
|
||||
try
|
||||
{
|
||||
if(mPolyphaseChannelOutputProcessor != null)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsList, currentSamplesTimestamp);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
mOutputProcessorLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,7 +277,6 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
*/
|
||||
protected void setChannelFrequencyCorrection(long value)
|
||||
{
|
||||
//TODO: push this frequency correction down to the output processor ...
|
||||
mChannelFrequencyCorrection = value;
|
||||
updateFrequencyOffset();
|
||||
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
|
||||
@@ -262,7 +312,10 @@ public class PolyphaseChannelSource extends TunerChannelSource implements Listen
|
||||
*/
|
||||
private void updateFrequencyOffset()
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
if(mPolyphaseChannelOutputProcessor != null)
|
||||
{
|
||||
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.filter.channelizer;
|
||||
|
||||
import io.github.dsheirer.dsp.filter.FilterFactory;
|
||||
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Creates and caches channel output processor synthesis filters.
|
||||
*/
|
||||
public class SynthesisFilterManager
|
||||
{
|
||||
private static final int POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL = 9;
|
||||
private static final String SEPARATOR = "-";
|
||||
private Map<String,float[]> mFilterMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Design or retrieve a previously cached output processor synthesis filter.
|
||||
* @param sampleRate of the tuner
|
||||
* @param channelBandwidth per channel
|
||||
* @param channelCount as the number of channels being synthesized/aggregated for the output processor (1 or 2)
|
||||
* @return filter
|
||||
* @throws FilterDesignException if the filter cannot be designed based on the supplied parameters.
|
||||
*/
|
||||
public float[] getFilter(double sampleRate, double channelBandwidth, int channelCount) throws FilterDesignException
|
||||
{
|
||||
String key = sampleRate + SEPARATOR + channelBandwidth + SEPARATOR + channelCount;
|
||||
|
||||
if(mFilterMap.containsKey(key))
|
||||
{
|
||||
return mFilterMap.get(key);
|
||||
}
|
||||
|
||||
float[] taps = FilterFactory.getSincM2Synthesizer(sampleRate, channelBandwidth, channelCount,
|
||||
POLYPHASE_SYNTHESIZER_TAPS_PER_CHANNEL);
|
||||
mFilterMap.put(key, taps);
|
||||
return taps;
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ public class TwoChannelSynthesizerM2
|
||||
mTopBlockFlag = !mTopBlockFlag;
|
||||
}
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, channelBuffer1.timestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,11 +21,10 @@ package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.util.Dispatcher;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor
|
||||
{
|
||||
@@ -34,6 +33,7 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
||||
private Dispatcher<List<float[]>> mChannelResultsDispatcher;
|
||||
protected Listener<ComplexSamples> mComplexSamplesListener;
|
||||
private int mInputChannelCount;
|
||||
private long mCurrentSampleTimestamp = System.currentTimeMillis();
|
||||
|
||||
/**
|
||||
* Base class for polyphase channelizer output channel processing. Provides built-in frequency translation
|
||||
@@ -50,6 +50,15 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
||||
mChannelResultsDispatcher.setListener(floats -> process(floats));
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp for the current series of samples.
|
||||
* @return time in milliseconds to use with assembled complex sample buffers.
|
||||
*/
|
||||
protected long getCurrentSampleTimestamp()
|
||||
{
|
||||
return mCurrentSampleTimestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start()
|
||||
{
|
||||
@@ -82,9 +91,10 @@ public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputP
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveChannelResults(List<float[]> channelResultsList)
|
||||
public void receiveChannelResults(List<float[]> channelResultsList, long timestamp)
|
||||
{
|
||||
mChannelResultsDispatcher.receive(channelResultsList);
|
||||
mCurrentSampleTimestamp = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,7 +20,6 @@ package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IPolyphaseChannelOutputProcessor
|
||||
@@ -37,19 +36,15 @@ public interface IPolyphaseChannelOutputProcessor
|
||||
/**
|
||||
* Receive and enqueue output results from the polyphase analysis channelizer
|
||||
* @param channelResults to enqueue
|
||||
* @param timestamp for the first channel results buffer
|
||||
*/
|
||||
void receiveChannelResults(List<float[]> channelResults);
|
||||
void receiveChannelResults(List<float[]> channelResults, long timestamp);
|
||||
|
||||
/**
|
||||
* Listener to receive assembled complex samples buffers
|
||||
*/
|
||||
void setListener(Listener<ComplexSamples> listener);
|
||||
|
||||
// /**
|
||||
// * Process the channel output channel results queue and deliver the output to the listener
|
||||
// */
|
||||
// void processChannelResults();
|
||||
|
||||
/**
|
||||
* Sets the desired frequency offset from center. The samples will be mixed with an oscillator set to this offset
|
||||
* frequency to produce an output where the desired signal is centered in the passband.
|
||||
|
||||
@@ -65,7 +65,8 @@ public abstract class MixerAssembler
|
||||
|
||||
/**
|
||||
* Access the fully assembled buffer when hasBuffer() indicates that a buffer is ready.
|
||||
* @param timestamp to use for the assembled complex samples buffer
|
||||
* @return true if a full buffer is assembled and ready.
|
||||
*/
|
||||
public abstract ComplexSamples getBuffer();
|
||||
public abstract ComplexSamples getBuffer(long timestamp);
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ public class OneChannelMixerAssembler extends MixerAssembler
|
||||
* @return assembled buffer.
|
||||
*/
|
||||
@Override
|
||||
public ComplexSamples getBuffer()
|
||||
public ComplexSamples getBuffer(long timestamp)
|
||||
{
|
||||
ComplexSamples buffer = mSampleAssembler.getBufferAndReset();
|
||||
ComplexSamples buffer = mSampleAssembler.getBufferAndReset(timestamp);
|
||||
|
||||
if(getMixer().hasFrequency())
|
||||
{
|
||||
|
||||
@@ -19,11 +19,10 @@
|
||||
package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(OneChannelOutputProcessor.class);
|
||||
@@ -80,7 +79,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
* Extract the channel from the channel results array and pass to the assembler. The assembler will
|
||||
* apply frequency translation and gain and indicate when a buffer is fully assembled.
|
||||
*
|
||||
* @param lists to process containing a list of a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
* @param channelResultsList to process containing a list of a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
*/
|
||||
@Override
|
||||
public void process(List<float[]> channelResultsList)
|
||||
@@ -91,7 +90,7 @@ public class OneChannelOutputProcessor extends ChannelOutputProcessor
|
||||
|
||||
if(mMixerAssembler.hasBuffer())
|
||||
{
|
||||
ComplexSamples buffer = mMixerAssembler.getBuffer();
|
||||
ComplexSamples buffer = mMixerAssembler.getBuffer(getCurrentSampleTimestamp());
|
||||
|
||||
if(mComplexSamplesListener != null)
|
||||
{
|
||||
|
||||
@@ -76,9 +76,9 @@ public class SampleAssembler
|
||||
/**
|
||||
* Extracts the full buffer from this assembler and resets the assembler to accept more samples.
|
||||
*/
|
||||
public ComplexSamples getBufferAndReset()
|
||||
public ComplexSamples getBufferAndReset(long timestamp)
|
||||
{
|
||||
ComplexSamples buffer = new ComplexSamples(mI, mQ);
|
||||
ComplexSamples buffer = new ComplexSamples(mI, mQ, timestamp);
|
||||
reset();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -76,13 +76,14 @@ public class TwoChannelMixerAssembler extends MixerAssembler
|
||||
|
||||
/**
|
||||
* Access the assembled buffer. Frequency offset and gain are applied to the returned buffer.
|
||||
* @param timestamp to use for the assembled buffer
|
||||
* @return assembled buffer.
|
||||
*/
|
||||
@Override
|
||||
public ComplexSamples getBuffer()
|
||||
public ComplexSamples getBuffer(long timestamp)
|
||||
{
|
||||
ComplexSamples channel1 = mChannel1SampleAssembler.getBufferAndReset();
|
||||
ComplexSamples channel2 = mChannel2SampleAssembler.getBufferAndReset();
|
||||
ComplexSamples channel1 = mChannel1SampleAssembler.getBufferAndReset(timestamp);
|
||||
ComplexSamples channel2 = mChannel2SampleAssembler.getBufferAndReset(timestamp);
|
||||
|
||||
//Synthesize two channels into one, reducing sample rate by 2 so it's same as single channel rate
|
||||
ComplexSamples buffer = mTwoChannelSynthesizer.process(channel1, channel2);
|
||||
|
||||
@@ -19,11 +19,10 @@
|
||||
package io.github.dsheirer.dsp.filter.channelizer.output;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
|
||||
@@ -91,7 +90,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
* Extract the channel from the channel results array, apply frequency translation, and deliver the
|
||||
* extracted frequency-corrected channel I/Q sample set to the complex sample listener.
|
||||
*
|
||||
* @param lists to process containing a list of a list of an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
* @param channelResultsList to process containing a list of a list of an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
|
||||
*/
|
||||
@Override
|
||||
public void process(List<float[]> channelResultsList)
|
||||
@@ -103,7 +102,7 @@ public class TwoChannelOutputProcessor extends ChannelOutputProcessor
|
||||
|
||||
if(mMixerAssembler.hasBuffer())
|
||||
{
|
||||
ComplexSamples buffer = mMixerAssembler.getBuffer();
|
||||
ComplexSamples buffer = mMixerAssembler.getBuffer(getCurrentSampleTimestamp());
|
||||
|
||||
if(mComplexSamplesListener != null)
|
||||
{
|
||||
|
||||
@@ -80,9 +80,10 @@ public abstract class HilbertTransform
|
||||
/**
|
||||
* Converts the real sample array to complex samples as half the sample rate
|
||||
* @param samples to convert
|
||||
* @param timestamp of the first sample
|
||||
* @return converted samples
|
||||
*/
|
||||
public abstract ComplexSamples filter(float[] samples);
|
||||
public abstract ComplexSamples filter(float[] samples, long timestamp);
|
||||
|
||||
/**
|
||||
* Converts the half-band filter coefficients for use as hilbert transform filter coefficients. Sets all
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.dsp.filter.hilbert;
|
||||
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class LegacyHilbertTransform
|
||||
{
|
||||
private static final Logger mLog = LoggerFactory.getLogger(LegacyHilbertTransform.class);
|
||||
|
||||
private float[] mHilbertFilter;
|
||||
private float[] mBuffer;
|
||||
private int mBufferSize;
|
||||
private int mBufferPointer;
|
||||
private int[][] mIndexMap;
|
||||
private int mMapHeight;
|
||||
private int mCenterTapIndex;
|
||||
private float mCenterCoefficent;
|
||||
|
||||
private boolean mInvertFlag = false;
|
||||
|
||||
/**
|
||||
* Hilbert transform filter used for converting real-valued samples into
|
||||
* complex valued samples using frequency translation (FS/4) and a half-band
|
||||
* filter.
|
||||
*
|
||||
* This filter uses a circular sample buffer and a pre-calculated
|
||||
* index map to correctly map the filter coefficients to the circular delay
|
||||
* buffer contents as each new sample is added to the buffer and processed.
|
||||
*
|
||||
* Half-band filter coefficients used in this filter must be of length N
|
||||
* where (N + 1) is a multiple of 4. This filter uses a pre-defined half
|
||||
* band filter with 47 coefficients.
|
||||
*
|
||||
* This transform process is described in Understanding Digital Signal
|
||||
* Processing, Lyons, 3e, 2011, sections 13.1.2 and 13.1.3 (p 674-678) and
|
||||
* implemented as described in Section 13.37.1 and 13.37.2 (p 802-804)
|
||||
*/
|
||||
public LegacyHilbertTransform()
|
||||
{
|
||||
convertHalfBandToHilbert(HilbertTransform.HALF_BAND_FILTER_47_TAP);
|
||||
|
||||
mBufferSize = mHilbertFilter.length + 1;
|
||||
mBuffer = new float[mBufferSize];
|
||||
|
||||
mCenterCoefficent = mHilbertFilter[mHilbertFilter.length / 2];
|
||||
|
||||
generateIndexMap(mHilbertFilter.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the continuous real-sample stream by treating the stream as a
|
||||
* set of interleaved values and applying two hilbert filters against each
|
||||
* interleaved set (I & Q). Frequency translation has already been
|
||||
* pre-applied to the hilbert filter coefficients.
|
||||
*
|
||||
* Filtering of the inphase component is a simple multiply by the center
|
||||
* coefficient (0.5) since all of the other coefficients are zero-valued.
|
||||
*
|
||||
* Filtering of the quadrature component uses a folded FIR structure and
|
||||
* expects the hilbert coefficients to be symmetric with all negative valued
|
||||
* coefficients below the center coefficient and all positive-valued coefficients
|
||||
* above the center coefficient. So, convolution only applies the filter
|
||||
* coefficient against the difference of the two samples.
|
||||
*
|
||||
* ( coefficient * value1 ) + (-coefficient * value2 ) = coefficient * ( value1 - value2 )
|
||||
*
|
||||
* Performs frequency translation by FS/2 on the final filtered values by
|
||||
* applying a sequence of 1,-1 (sign change) to each I/Q output sample.
|
||||
*/
|
||||
public float[] filter(float[] samples)
|
||||
{
|
||||
float accumulator;
|
||||
|
||||
for(int y = 0; y < samples.length; y += 2)
|
||||
{
|
||||
insert(samples[y]);
|
||||
insert(samples[y + 1]);
|
||||
|
||||
accumulator = 0.0f;
|
||||
|
||||
int index = mBufferPointer / 2;
|
||||
|
||||
for(int x = 0; x < mHilbertFilter.length / 2; x += 2)
|
||||
{
|
||||
accumulator += mHilbertFilter[x] *
|
||||
(mBuffer[mIndexMap[index][x + 1]] -
|
||||
mBuffer[mIndexMap[index][x]]);
|
||||
}
|
||||
|
||||
//Perform FS/2 frequency translation on the final filtered values
|
||||
if(mInvertFlag)
|
||||
{
|
||||
//inphase
|
||||
samples[y] = -(mBuffer[mIndexMap[index][mCenterTapIndex]]);
|
||||
|
||||
//quadrature
|
||||
samples[y + 1] = -accumulator;
|
||||
}
|
||||
else
|
||||
{
|
||||
//inphase
|
||||
samples[y] = mBuffer[mIndexMap[index][mCenterTapIndex]];
|
||||
|
||||
//quadrature
|
||||
samples[y + 1] = accumulator;
|
||||
}
|
||||
|
||||
mInvertFlag = !mInvertFlag;
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the sample into the circular buffer, overwriting the oldest value
|
||||
*/
|
||||
private void insert(float sample)
|
||||
{
|
||||
mBuffer[mBufferPointer++] = sample;
|
||||
|
||||
mBufferPointer = mBufferPointer % mBufferSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an index map to support efficient lookup of sample indexes from
|
||||
* the circular buffer.
|
||||
*
|
||||
* The first index of the map corresponds to the current buffer pointer
|
||||
* setting.
|
||||
*
|
||||
* The second index is structured in sample pairs to align with
|
||||
* the hilbert filter coefficients. For example, the first iteration of
|
||||
* the filter would use filter coefficient 0 and the index map values 0 and
|
||||
* 1 would be pointing to samples 46 and 0.
|
||||
*
|
||||
* The final element of each index set is the index of the center sample.
|
||||
*/
|
||||
private void generateIndexMap(int size)
|
||||
{
|
||||
mMapHeight = size / 2 + 1;
|
||||
int mapWidth = mMapHeight + 1;
|
||||
|
||||
mIndexMap = new int[mMapHeight][mapWidth];
|
||||
|
||||
//Setup the first row with buffer pointer index 0 as a starting point
|
||||
for(int x = 0; x < mapWidth - 1; x += 2)
|
||||
{
|
||||
mIndexMap[0][x] = size - 1 - x;
|
||||
mIndexMap[0][x + 1] = x;
|
||||
}
|
||||
|
||||
//Place the center index at the end of the array
|
||||
mCenterTapIndex = mapWidth - 1;
|
||||
|
||||
mIndexMap[0][mCenterTapIndex] = size / 2;
|
||||
|
||||
//For each subsequent row, increment the previous row's value by 2,
|
||||
//wrapping as needed, to keep the values between 0 and size - 1
|
||||
for(int x = 1; x < mMapHeight; x++)
|
||||
{
|
||||
for(int y = 0; y < mapWidth; y++)
|
||||
{
|
||||
mIndexMap[x][y] = mIndexMap[x - 1][y] + 2;
|
||||
|
||||
if(mIndexMap[x][y] >= size)
|
||||
{
|
||||
mIndexMap[x][y] -= size + 1;
|
||||
|
||||
//Special handling for center tap wrap around
|
||||
if(y == mCenterTapIndex && mIndexMap[x][y] < 0)
|
||||
{
|
||||
mIndexMap[x][y] = size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs the index map contents
|
||||
*/
|
||||
public void logIndexMap()
|
||||
{
|
||||
for(int[] indexSet : mIndexMap)
|
||||
{
|
||||
mLog.debug("Row:" + Arrays.toString(indexSet));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the half-band filter coefficients for use as hilbert transform
|
||||
* filter coefficients. Sets all even-numbered coefficients left of center
|
||||
* to negative and all even-numbered coefficients right of center to positive
|
||||
* which applies a FS/4 frequency translation to the coefficients.
|
||||
*
|
||||
* Note: even though the coefficients above the center coefficient are being
|
||||
* defined, they are unused during the convolution process since we're using
|
||||
* a folded FIR structure to exploit the symmetric nature of the half-band
|
||||
* filter coefficients. The polarity difference between the upper and lower
|
||||
* halves is accounted for in the filter() method.
|
||||
*
|
||||
* Note: we apply a 2.0 gain to the coefficients to compensate for the loss
|
||||
* of splitting the signal via two filters.
|
||||
*/
|
||||
private void convertHalfBandToHilbert(float[] coefficients)
|
||||
{
|
||||
mHilbertFilter = new float[coefficients.length];
|
||||
|
||||
int middle = coefficients.length / 2;
|
||||
|
||||
for(int x = 0; x < coefficients.length; x++)
|
||||
{
|
||||
if(x < middle)
|
||||
{
|
||||
mHilbertFilter[x] = 2.0f * -FastMath.abs(coefficients[x]);
|
||||
}
|
||||
else if(x > middle)
|
||||
{
|
||||
mHilbertFilter[x] = 2.0f * FastMath.abs(coefficients[x]);
|
||||
}
|
||||
else
|
||||
{
|
||||
mHilbertFilter[x] = 2.0f * coefficients[x];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import io.github.dsheirer.dsp.oscillator.IRealOscillator;
|
||||
import io.github.dsheirer.dsp.oscillator.OscillatorFactory;
|
||||
import io.github.dsheirer.sample.SampleUtils;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@@ -35,9 +34,10 @@ public class ScalarHilbertTransform extends HilbertTransform
|
||||
/**
|
||||
* Filters the real samples array into complex samples at half the sample rate.
|
||||
* @param realSamples to filter
|
||||
* @param timestamp of the first sample
|
||||
* @return complex samples at half the sample rate
|
||||
*/
|
||||
public ComplexSamples filter(float[] realSamples)
|
||||
public ComplexSamples filter(float[] realSamples, long timestamp)
|
||||
{
|
||||
int bufferLength = realSamples.length / 2;
|
||||
|
||||
@@ -52,7 +52,7 @@ public class ScalarHilbertTransform extends HilbertTransform
|
||||
mQBuffer = qTemp;
|
||||
}
|
||||
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
|
||||
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
|
||||
System.arraycopy(deinterleaved.q(), 0, mQBuffer, mQOverlap, deinterleaved.q().length);
|
||||
|
||||
@@ -78,7 +78,7 @@ public class ScalarHilbertTransform extends HilbertTransform
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
@@ -104,8 +104,8 @@ public class ScalarHilbertTransform extends HilbertTransform
|
||||
|
||||
if(validate)
|
||||
{
|
||||
ComplexSamples scalar = hilbertTransform.filter(realSamples);
|
||||
ComplexSamples vector = vector512.filter(realSamples);
|
||||
ComplexSamples scalar = hilbertTransform.filter(realSamples, 0l);
|
||||
ComplexSamples vector = vector512.filter(realSamples, start);
|
||||
|
||||
System.out.println("SI: " + Arrays.toString(scalar.i()));
|
||||
System.out.println("VI: " + Arrays.toString(vector.i()));
|
||||
@@ -119,7 +119,7 @@ public class ScalarHilbertTransform extends HilbertTransform
|
||||
{
|
||||
// ComplexSamples filtered = hilbertTransform.filter(realSamples);
|
||||
// ComplexSamples filtered = vectorDefault.filter(realSamples);
|
||||
ComplexSamples filtered = vector512.filter(realSamples);
|
||||
ComplexSamples filtered = vector512.filter(realSamples, start);
|
||||
// ComplexSamples filtered = vector256.filter(realSamples);
|
||||
// ComplexSamples filtered = vector128.filter(realSamples);
|
||||
// ComplexSamples filtered = vector64.filter(realSamples);
|
||||
|
||||
@@ -37,9 +37,10 @@ public class VectorHilbertTransform128Bits extends HilbertTransform
|
||||
/**
|
||||
* Filters the real samples array into complex samples at half the sample rate.
|
||||
* @param realSamples to filter
|
||||
* @param timestamp of the first sample
|
||||
* @return complex samples at half the sample rate
|
||||
*/
|
||||
public ComplexSamples filter(float[] realSamples)
|
||||
public ComplexSamples filter(float[] realSamples, long timestamp)
|
||||
{
|
||||
int bufferLength = realSamples.length / 2;
|
||||
|
||||
@@ -54,7 +55,7 @@ public class VectorHilbertTransform128Bits extends HilbertTransform
|
||||
mQBuffer = qTemp;
|
||||
}
|
||||
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
|
||||
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
|
||||
|
||||
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
|
||||
@@ -91,6 +92,6 @@ public class VectorHilbertTransform128Bits extends HilbertTransform
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,10 @@ public class VectorHilbertTransform256Bits extends HilbertTransform
|
||||
/**
|
||||
* Filters the real samples array into complex samples at half the sample rate.
|
||||
* @param realSamples to filter
|
||||
* @param timestamp of the first sample
|
||||
* @return complex samples at half the sample rate
|
||||
*/
|
||||
public ComplexSamples filter(float[] realSamples)
|
||||
public ComplexSamples filter(float[] realSamples, long timestamp)
|
||||
{
|
||||
int bufferLength = realSamples.length / 2;
|
||||
|
||||
@@ -54,7 +55,7 @@ public class VectorHilbertTransform256Bits extends HilbertTransform
|
||||
mQBuffer = qTemp;
|
||||
}
|
||||
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
|
||||
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
|
||||
|
||||
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
|
||||
@@ -85,6 +86,6 @@ public class VectorHilbertTransform256Bits extends HilbertTransform
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,11 @@ package io.github.dsheirer.dsp.filter.hilbert;
|
||||
import io.github.dsheirer.sample.SampleUtils;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorOperators;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Implements a Hilbert transform using vector SIMD operations against a pre-defined 47-tap filter.
|
||||
* As described in Understanding Digital Signal Processing, Lyons, 3e, 2011, Section 13.37.2 (p 804-805)
|
||||
@@ -53,9 +52,10 @@ public class VectorHilbertTransform512Bits extends HilbertTransform
|
||||
/**
|
||||
* Filters the real samples array into complex samples at half the sample rate.
|
||||
* @param realSamples to filter
|
||||
* @param timestamp of the first sample
|
||||
* @return complex samples at half the sample rate
|
||||
*/
|
||||
public ComplexSamples filter(float[] realSamples)
|
||||
public ComplexSamples filter(float[] realSamples, long timestamp)
|
||||
{
|
||||
int bufferLength = realSamples.length / 2;
|
||||
|
||||
@@ -70,7 +70,7 @@ public class VectorHilbertTransform512Bits extends HilbertTransform
|
||||
mQBuffer = qTemp;
|
||||
}
|
||||
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
|
||||
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
|
||||
|
||||
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
|
||||
@@ -103,6 +103,6 @@ public class VectorHilbertTransform512Bits extends HilbertTransform
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,10 @@ public class VectorHilbertTransform64Bits extends HilbertTransform
|
||||
/**
|
||||
* Filters the real samples array into complex samples at half the sample rate.
|
||||
* @param realSamples to filter
|
||||
* @param timestamp of the first sample
|
||||
* @return complex samples at half the sample rate
|
||||
*/
|
||||
public ComplexSamples filter(float[] realSamples)
|
||||
public ComplexSamples filter(float[] realSamples, long timestamp)
|
||||
{
|
||||
int bufferLength = realSamples.length / 2;
|
||||
|
||||
@@ -54,7 +55,7 @@ public class VectorHilbertTransform64Bits extends HilbertTransform
|
||||
mQBuffer = qTemp;
|
||||
}
|
||||
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
|
||||
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
|
||||
|
||||
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
|
||||
@@ -103,6 +104,6 @@ public class VectorHilbertTransform64Bits extends HilbertTransform
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,10 @@ public class VectorHilbertTransformDefaultBits extends HilbertTransform
|
||||
/**
|
||||
* Filters the real samples array into complex samples at half the sample rate.
|
||||
* @param realSamples to filter
|
||||
* @param timestamp of the first sample
|
||||
* @return complex samples at half the sample rate
|
||||
*/
|
||||
public ComplexSamples filter(float[] realSamples)
|
||||
public ComplexSamples filter(float[] realSamples, long timestamp)
|
||||
{
|
||||
int bufferLength = realSamples.length / 2;
|
||||
|
||||
@@ -54,7 +55,7 @@ public class VectorHilbertTransformDefaultBits extends HilbertTransform
|
||||
mQBuffer = qTemp;
|
||||
}
|
||||
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples);
|
||||
ComplexSamples deinterleaved = SampleUtils.deinterleave(realSamples, timestamp);
|
||||
VectorUtilities.checkComplexArrayLength(deinterleaved.i(), deinterleaved.q(), VECTOR_SPECIES);
|
||||
|
||||
System.arraycopy(deinterleaved.i(), 0, mIBuffer, mIOverlap, deinterleaved.i().length);
|
||||
@@ -84,6 +85,6 @@ public class VectorHilbertTransformDefaultBits extends HilbertTransform
|
||||
System.arraycopy(mIBuffer, mIBuffer.length - mIOverlap, mIBuffer, 0, mIOverlap);
|
||||
System.arraycopy(mQBuffer, mQBuffer.length - mQOverlap, mQBuffer, 0, mQOverlap);
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.gain.complex;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -46,7 +65,7 @@ public abstract class ComplexGain
|
||||
*/
|
||||
public ComplexSamples apply(ComplexSamples samples)
|
||||
{
|
||||
return apply(samples.i(), samples.q());
|
||||
return apply(samples.i(), samples.q(), samples.timestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +75,8 @@ public abstract class ComplexGain
|
||||
* original sample buffer.
|
||||
* @param i samples to amplify
|
||||
* @param q samples to amplify
|
||||
* @param timestamp of the first sample
|
||||
* @return amplified samples
|
||||
*/
|
||||
public abstract ComplexSamples apply(float[] i, float q[]);
|
||||
public abstract ComplexSamples apply(float[] i, float q[], long timestamp);
|
||||
}
|
||||
|
||||
@@ -18,13 +18,9 @@
|
||||
*/
|
||||
package io.github.dsheirer.dsp.gain.complex;
|
||||
|
||||
import io.github.dsheirer.sample.SampleUtils;
|
||||
import io.github.dsheirer.sample.complex.Complex;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ComplexGainControl implements IComplexGainControl
|
||||
{
|
||||
public static final float OBJECTIVE_ENVELOPE = 1.0f;
|
||||
@@ -48,9 +44,10 @@ public class ComplexGainControl implements IComplexGainControl
|
||||
* gain for the single sample in the buffer that has the largest envelope.
|
||||
* @param i samples
|
||||
* @param q samples
|
||||
* @param timestamp of the first sample
|
||||
* @return processed/amplified buffer
|
||||
*/
|
||||
@Override public ComplexSamples process(float[] i, float[] q)
|
||||
@Override public ComplexSamples process(float[] i, float[] q, long timestamp)
|
||||
{
|
||||
float maxEnvelope = MINIMUM_ENVELOPE;
|
||||
|
||||
@@ -70,45 +67,6 @@ public class ComplexGainControl implements IComplexGainControl
|
||||
qProcessed[x] = q[x] * gain;
|
||||
}
|
||||
|
||||
return new ComplexSamples(iProcessed, qProcessed);
|
||||
}
|
||||
|
||||
public static void main(String[] args)
|
||||
{
|
||||
int sampleSize = 2048;
|
||||
ComplexSamples samples = SampleUtils.generateComplexRandomVectorLength(sampleSize);
|
||||
ComplexGainControl scalar = new ComplexGainControl();
|
||||
VectorComplexGainControl vector = new VectorComplexGainControl();
|
||||
|
||||
boolean validate = false;
|
||||
|
||||
if(validate)
|
||||
{
|
||||
ComplexSamples scalarSamples = scalar.process(samples.i(), samples.q());
|
||||
ComplexSamples vectorSamples = vector.process(samples.i(), samples.q());
|
||||
|
||||
System.out.println("SCALAR I: " + Arrays.toString(scalarSamples.i()));
|
||||
System.out.println("VECTOR I: " + Arrays.toString(vectorSamples.i()));
|
||||
System.out.println("SCALAR Q: " + Arrays.toString(scalarSamples.q()));
|
||||
System.out.println("VECTOR Q: " + Arrays.toString(vectorSamples.q()));
|
||||
}
|
||||
else
|
||||
{
|
||||
double accumulator = 0.0d;
|
||||
int iterations = 10_000_000;
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
for(int x = 0; x < iterations; x++)
|
||||
{
|
||||
// ComplexSamples processed = scalar.process(samples.i(), samples.q());
|
||||
ComplexSamples processed = vector.process(samples.i(), samples.q());
|
||||
accumulator += processed.i()[3];
|
||||
}
|
||||
|
||||
double elapsed = System.currentTimeMillis() - start;
|
||||
DecimalFormat df = new DecimalFormat("0.000");
|
||||
System.out.println("Accumulator: " + accumulator);
|
||||
System.out.println("Test Complete. Elapsed Time: " + df.format(elapsed / 1000.0d) + " seconds");
|
||||
}
|
||||
return new ComplexSamples(iProcessed, qProcessed, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.gain.complex;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -13,5 +32,5 @@ public interface IComplexGainControl
|
||||
* @param q samples
|
||||
* @return amplified gain samples
|
||||
*/
|
||||
ComplexSamples process(float[] i, float[] q);
|
||||
ComplexSamples process(float[] i, float[] q, long timestamp);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.gain.complex;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -22,7 +41,7 @@ public class ScalarComplexGain extends ComplexGain
|
||||
* @param q samples to amplify
|
||||
* @return amplified samples
|
||||
*/
|
||||
@Override public ComplexSamples apply(float[] i, float[] q)
|
||||
@Override public ComplexSamples apply(float[] i, float[] q, long timestamp)
|
||||
{
|
||||
for(int x = 0; x < i.length; x++)
|
||||
{
|
||||
@@ -30,6 +49,6 @@ public class ScalarComplexGain extends ComplexGain
|
||||
q[x] *= mGain;
|
||||
}
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.gain.complex;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -27,7 +46,7 @@ public class VectorComplexGain extends ComplexGain
|
||||
* @param q samples to amplify
|
||||
* @return amplified samples.
|
||||
*/
|
||||
@Override public ComplexSamples apply(float[] i, float[] q)
|
||||
@Override public ComplexSamples apply(float[] i, float[] q, long timestamp)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
|
||||
@@ -37,6 +56,6 @@ public class VectorComplexGain extends ComplexGain
|
||||
FloatVector.fromArray(VECTOR_SPECIES, q, x).mul(mGain).intoArray(q, x);
|
||||
}
|
||||
|
||||
return new ComplexSamples(i, q);
|
||||
return new ComplexSamples(i, q, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.gain.complex;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -18,7 +37,7 @@ public class VectorComplexGainControl implements IComplexGainControl
|
||||
private static final float MINIMUM_ENVELOPE = 0.0001f;
|
||||
private static final float ENVELOPE_ESTIMATE = 0.4f;
|
||||
|
||||
@Override public ComplexSamples process(float[] i, float[] q)
|
||||
@Override public ComplexSamples process(float[] i, float[] q, long timestamp)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(i, q, VECTOR_SPECIES);
|
||||
|
||||
@@ -47,6 +66,6 @@ public class VectorComplexGainControl implements IComplexGainControl
|
||||
FloatVector.fromArray(VECTOR_SPECIES, q, bufferPointer).mul(gain).intoArray(qProcessed, bufferPointer);
|
||||
}
|
||||
|
||||
return new ComplexSamples(iProcessed, qProcessed);
|
||||
return new ComplexSamples(iProcessed, qProcessed, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +84,12 @@ public abstract class ComplexMixer
|
||||
/**
|
||||
* Generates complex samples from the underlying oscillator
|
||||
* @param sampleCount to generate
|
||||
* @param timestamp of the first sample
|
||||
* @return complex samples
|
||||
*/
|
||||
protected ComplexSamples generate(int sampleCount)
|
||||
protected ComplexSamples generate(int sampleCount, long timestamp)
|
||||
{
|
||||
return mOscillator.generateComplexSamples(sampleCount);
|
||||
return mOscillator.generateComplexSamples(sampleCount, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,7 +99,7 @@ public abstract class ComplexMixer
|
||||
*/
|
||||
public ComplexSamples mix(ComplexSamples samples)
|
||||
{
|
||||
return mix(samples.i(), samples.q());
|
||||
return mix(samples.i(), samples.q(), samples.timestamp());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +118,5 @@ public abstract class ComplexMixer
|
||||
* @param q complex samples to mix
|
||||
* @return mixed samples
|
||||
*/
|
||||
public abstract ComplexSamples mix(float[] i, float[] q);
|
||||
|
||||
|
||||
public abstract ComplexSamples mix(float[] i, float[] q, long timestamp);
|
||||
}
|
||||
|
||||
@@ -52,11 +52,12 @@ public class ScalarComplexMixer extends ComplexMixer
|
||||
* Mixes the complex I & Q samples with samples generated from an oscillator.
|
||||
* @param iSamples complex samples to mix
|
||||
* @param qSamples complex samples to mix
|
||||
* @param timestamp of the first sample
|
||||
* @return mixed samples
|
||||
*/
|
||||
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples)
|
||||
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp)
|
||||
{
|
||||
ComplexSamples mixer = generate(iSamples.length);
|
||||
ComplexSamples mixer = generate(iSamples.length, timestamp);
|
||||
|
||||
float[] iMixer = mixer.i();
|
||||
float[] qMixer = mixer.q();
|
||||
@@ -82,7 +83,7 @@ public class ScalarComplexMixer extends ComplexMixer
|
||||
@Override
|
||||
public ComplexSamples mix(InterleavedComplexSamples samples)
|
||||
{
|
||||
ComplexSamples mixer = generate(samples.samples().length / 2);
|
||||
ComplexSamples mixer = generate(samples.samples().length / 2, samples.timestamp());
|
||||
|
||||
float[] iMixer = mixer.i();
|
||||
float[] qMixer = mixer.q();
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
|
||||
package io.github.dsheirer.dsp.mixer;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
@@ -26,14 +45,15 @@ public class VectorComplexMixer extends ComplexMixer
|
||||
* Mixes the complex I & Q samples with samples generated from an oscillator.
|
||||
* @param iSamples complex samples to mix
|
||||
* @param qSamples complex samples to mix
|
||||
* @param timestamp for the first sample
|
||||
* @return mixed samples
|
||||
*/
|
||||
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples)
|
||||
@Override public ComplexSamples mix(float[] iSamples, float[] qSamples, long timestamp)
|
||||
{
|
||||
VectorUtilities.checkComplexArrayLength(iSamples, qSamples, VECTOR_SPECIES);
|
||||
|
||||
//Reuse this complex samples buffer to store the results and return to caller
|
||||
ComplexSamples mixer = generate(iSamples.length);
|
||||
ComplexSamples mixer = generate(iSamples.length, timestamp);
|
||||
|
||||
float[] iMixer = mixer.i();
|
||||
float[] qMixer = mixer.q();
|
||||
|
||||
@@ -33,7 +33,8 @@ public interface IComplexOscillator extends IOscillator
|
||||
/**
|
||||
* Generates the specified number of complex samples into a complex samples object.
|
||||
* @param sampleCount number of complex samples to generate.
|
||||
* @param timestamp for the first sample
|
||||
* @return generated samples
|
||||
*/
|
||||
ComplexSamples generateComplexSamples(int sampleCount);
|
||||
ComplexSamples generateComplexSamples(int sampleCount, long timestamp);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class ScalarComplexOscillator extends AbstractOscillator implements IComp
|
||||
}
|
||||
|
||||
@Override
|
||||
public ComplexSamples generateComplexSamples(int sampleCount)
|
||||
public ComplexSamples generateComplexSamples(int sampleCount, long timestamp)
|
||||
{
|
||||
float[] iSamples = new float[sampleCount];
|
||||
float[] qSamples = new float[sampleCount];
|
||||
@@ -133,6 +133,6 @@ public class ScalarComplexOscillator extends AbstractOscillator implements IComp
|
||||
mPreviousInphase = previousInphase;
|
||||
mPreviousQuadrature = previousQuadrature;
|
||||
|
||||
return new ComplexSamples(iSamples, qSamples);
|
||||
return new ComplexSamples(iSamples, qSamples, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,13 @@ package io.github.dsheirer.dsp.oscillator;
|
||||
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.vector.VectorUtilities;
|
||||
import java.util.Arrays;
|
||||
import jdk.incubator.vector.FloatVector;
|
||||
import jdk.incubator.vector.VectorSpecies;
|
||||
import org.apache.commons.math3.util.FastMath;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Complex oscillator that uses JDK17 SIMD vector operations to generate complex sample arrays.
|
||||
*
|
||||
@@ -145,9 +144,10 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp
|
||||
/**
|
||||
* Generates complex samples.
|
||||
* @param sampleCount number of samples to generate and length of the resulting float array.
|
||||
* @param timestamp of the first sample
|
||||
* @return generated samples
|
||||
*/
|
||||
@Override public ComplexSamples generateComplexSamples(int sampleCount)
|
||||
@Override public ComplexSamples generateComplexSamples(int sampleCount, long timestamp)
|
||||
{
|
||||
if(sampleCount % VECTOR_SPECIES.length() != 0)
|
||||
{
|
||||
@@ -184,6 +184,6 @@ public class VectorComplexOscillator extends AbstractOscillator implements IComp
|
||||
previousInphase.intoArray(mPreviousInphases, 0);
|
||||
previousQuadrature.intoArray(mPreviousQuadratures, 0);
|
||||
|
||||
return new ComplexSamples(iSamples, qSamples);
|
||||
return new ComplexSamples(iSamples, qSamples, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,10 @@ package io.github.dsheirer.dsp.symbol;
|
||||
import io.github.dsheirer.bits.IBinarySymbolProcessor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Assembles reusable byte buffers from an incoming stream of boolean values.
|
||||
*/
|
||||
@@ -58,6 +57,7 @@ public class BinaryToByteBufferAssembler implements IBinarySymbolProcessor, IByt
|
||||
{
|
||||
if(mCurrentBuffer != null && mBufferListener != null)
|
||||
{
|
||||
mCurrentBuffer.flip();
|
||||
mBufferListener.receive(mCurrentBuffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,10 @@ package io.github.dsheirer.dsp.symbol;
|
||||
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.buffer.IByteBufferProvider;
|
||||
import java.nio.ByteBuffer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Assembles reusable byte buffers from an incoming stream of Dibits.
|
||||
*/
|
||||
@@ -57,6 +56,7 @@ public class DibitToByteBufferAssembler implements Listener<Dibit>, IByteBufferP
|
||||
{
|
||||
if(mCurrentBuffer != null && mBufferListener != null)
|
||||
{
|
||||
mCurrentBuffer.flip();
|
||||
mBufferListener.receive(mCurrentBuffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ import io.github.dsheirer.spectrum.DFTSize;
|
||||
import io.github.dsheirer.spectrum.SpectrumPanel;
|
||||
import io.github.dsheirer.spectrum.converter.ComplexDecibelConverter;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -45,9 +48,6 @@ import javax.swing.JSpinner;
|
||||
import javax.swing.SpinnerNumberModel;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.EventQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class SynthesizerViewer extends JFrame
|
||||
{
|
||||
@@ -281,15 +281,15 @@ public class SynthesizerViewer extends JFrame
|
||||
@Override
|
||||
public void run()
|
||||
{
|
||||
ComplexSamples channel1Buffer = getChannel1ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle);
|
||||
ComplexSamples channel2Buffer = getChannel2ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle);
|
||||
ComplexSamples channel1Buffer = getChannel1ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle, 0l);
|
||||
ComplexSamples channel2Buffer = getChannel2ControlPanel().getOscillator().generateComplexSamples(mSamplesPerCycle, 0l);
|
||||
|
||||
ComplexSamples synthesizedBuffer = mSynthesizer.process(channel1Buffer, channel2Buffer);
|
||||
getChannel1Panel().receive(new FloatNativeBuffer(channel1Buffer, System.currentTimeMillis()));
|
||||
getChannel1Panel().receive(new FloatNativeBuffer(channel1Buffer));
|
||||
|
||||
getChannel2Panel().receive(new FloatNativeBuffer(channel2Buffer, System.currentTimeMillis()));
|
||||
getChannel2Panel().receive(new FloatNativeBuffer(channel2Buffer));
|
||||
|
||||
getSpectrumPanel().receive(new FloatNativeBuffer(synthesizedBuffer, System.currentTimeMillis()));
|
||||
getSpectrumPanel().receive(new FloatNativeBuffer(synthesizedBuffer));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,11 @@ import io.github.dsheirer.rrapi.type.Type;
|
||||
import io.github.dsheirer.rrapi.type.Voice;
|
||||
import io.github.dsheirer.service.radioreference.RadioReference;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -55,12 +60,6 @@ import jiconfont.javafx.IconNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Radio Reference editor for trunked radio systems
|
||||
*/
|
||||
@@ -293,7 +292,13 @@ public class SystemEditor extends VBox
|
||||
List<EnrichedSite> enrichedSites = new ArrayList<>();
|
||||
for(Site site: sites)
|
||||
{
|
||||
CountyInfo countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
|
||||
CountyInfo countyInfo = null;
|
||||
|
||||
if(site.getCountyId() > 0)
|
||||
{
|
||||
countyInfo = mRadioReference.getService().getCountyInfo(site.getCountyId());
|
||||
}
|
||||
|
||||
enrichedSites.add(new EnrichedSite(site, countyInfo));
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.mp3.MP3Preference;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
@@ -99,12 +100,47 @@ public class MP3PreferenceEditor extends HBox
|
||||
mMP3SettingComboBox.getItems().addAll(MP3Setting.values());
|
||||
mMP3SettingComboBox.getSelectionModel().select(mMP3Preference.getMP3Setting());
|
||||
mMP3SettingComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mMP3Preference.setMP3Setting(newValue));
|
||||
.addListener((observable, oldValue, newValue) -> {
|
||||
mMP3Preference.setMP3Setting(newValue);
|
||||
updateAudioSampleRateComboBox();
|
||||
});
|
||||
}
|
||||
|
||||
return mMP3SettingComboBox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the input sample rate combo box values and alerts the user whenever we auto-change an input sample rate
|
||||
* due to a change in the MP3 setting value.
|
||||
*/
|
||||
private void updateAudioSampleRateComboBox()
|
||||
{
|
||||
InputAudioFormat currentSelection = getAudioSampleRateComboBox().getSelectionModel().getSelectedItem();
|
||||
MP3Setting setting = getMP3SettingComboBox().getSelectionModel().getSelectedItem();
|
||||
|
||||
getAudioSampleRateComboBox().getItems().clear();
|
||||
getAudioSampleRateComboBox().getItems().addAll(setting.getSupportedSampleRates());
|
||||
|
||||
if(currentSelection != null && getAudioSampleRateComboBox().getItems().contains(currentSelection))
|
||||
{
|
||||
getAudioSampleRateComboBox().getSelectionModel().select(currentSelection);
|
||||
}
|
||||
else
|
||||
{
|
||||
getAudioSampleRateComboBox().getSelectionModel().select(InputAudioFormat.getDefault());
|
||||
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
alert.setTitle("Sample Rate Updated");
|
||||
alert.setHeaderText("Input Sample Rate Updated");
|
||||
|
||||
Label wrappingLabel = new Label("Previous input sample rate [" + currentSelection +
|
||||
"] is not supported with encoder setting [" + setting + "]. Sample rate updated to default.");
|
||||
wrappingLabel.setWrapText(true);
|
||||
alert.getDialogPane().setContent(wrappingLabel);
|
||||
alert.show();
|
||||
}
|
||||
}
|
||||
|
||||
private ComboBox<InputAudioFormat> getAudioSampleRateComboBox()
|
||||
{
|
||||
if(mAudioSampleRateComboBox == null)
|
||||
@@ -113,7 +149,12 @@ public class MP3PreferenceEditor extends HBox
|
||||
mAudioSampleRateComboBox.getItems().addAll(InputAudioFormat.values());
|
||||
mAudioSampleRateComboBox.getSelectionModel().select(mMP3Preference.getAudioSampleRate());
|
||||
mAudioSampleRateComboBox.getSelectionModel().selectedItemProperty()
|
||||
.addListener((observable, oldValue, newValue) -> mMP3Preference.setAudioSampleRate(newValue));
|
||||
.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null)
|
||||
{
|
||||
mMP3Preference.setAudioSampleRate(newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mAudioSampleRateComboBox;
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
/*
|
||||
* *********************************************************************************************************************
|
||||
* sdrtrunk
|
||||
* Copyright (C) 2014-2017 Dennis Sheirer
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
|
||||
* License as published by 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.icon;
|
||||
|
||||
@@ -22,15 +24,6 @@ import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
import io.github.dsheirer.properties.SystemProperties;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import java.awt.Image;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -43,6 +36,15 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
|
||||
public class IconModel
|
||||
{
|
||||
@@ -241,7 +243,6 @@ public class IconModel
|
||||
if(path != null)
|
||||
{
|
||||
Icon icon = new Icon("", path);
|
||||
|
||||
return getScaledIcon(icon.getIcon(), height);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* 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.identifier.tone;
|
||||
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IMessage wrapper for a ToneIdentifier decoded from a sequence of AMBE audio frames.
|
||||
* @param protocol of the decoded tone
|
||||
* @param timeslot where the tone occurred
|
||||
* @param timestamp of the event
|
||||
* @param toneIdentifier that was decoded
|
||||
* @param messageText to convey with the tone identifier
|
||||
*/
|
||||
public record ToneIdentifierMessage(Protocol protocol, int timeslot, long timestamp, ToneIdentifier toneIdentifier,
|
||||
String messageText) implements IMessage
|
||||
{
|
||||
@Override
|
||||
public long getTimestamp()
|
||||
{
|
||||
return timestamp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Protocol getProtocol()
|
||||
{
|
||||
return protocol();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimeslot()
|
||||
{
|
||||
return timeslot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Identifier> getIdentifiers()
|
||||
{
|
||||
return Collections.singletonList(toneIdentifier());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return messageText();
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,11 @@ import io.github.dsheirer.sample.complex.IComplexSamplesListener;
|
||||
import io.github.dsheirer.source.ISourceEventListener;
|
||||
import io.github.dsheirer.source.ISourceEventProvider;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* DMR decoder module.
|
||||
@@ -147,7 +146,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
mMessageFramer.setCurrentTime(System.currentTimeMillis());
|
||||
mMessageFramer.setCurrentTime(samples.timestamp());
|
||||
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
float[] q = mQBasebandFilter.filter(samples.q());
|
||||
@@ -155,7 +154,7 @@ public class DMRDecoder extends FeedbackDecoder implements ISourceEventListener,
|
||||
//Process buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,20 +53,20 @@ public class DMRDecoderInstrumented extends DMRDecoder
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
mMessageFramer.setCurrentTime(System.currentTimeMillis());
|
||||
mMessageFramer.setCurrentTime(samples.timestamp());
|
||||
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
float[] q = mQBasebandFilter.filter(samples.q());
|
||||
|
||||
if(mFilteredSymbolListener != null)
|
||||
{
|
||||
mFilteredSymbolListener.receive(new ComplexSamples(i, q));
|
||||
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
|
||||
}
|
||||
|
||||
//Process buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,14 +22,16 @@ import io.github.dsheirer.alias.AliasList;
|
||||
import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateProvider;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.tone.AmbeTone;
|
||||
import io.github.dsheirer.identifier.tone.Tone;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifierMessage;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.IMessageProvider;
|
||||
import io.github.dsheirer.module.decode.dmr.identifier.DMRToneIdentifier;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
|
||||
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
|
||||
@@ -40,19 +42,19 @@ import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
|
||||
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import jmbe.iface.IAudioWithMetadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DMR Audio Module for converting transmitted AMBE audio frames to 8 kHz PCM audio
|
||||
*/
|
||||
public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateProvider
|
||||
public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateProvider, IMessageProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(DMRAudioModule.class);
|
||||
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
|
||||
@@ -61,6 +63,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
private List<byte[]> mQueuedAmbeFrames = new ArrayList<>();
|
||||
private boolean mEncryptedCallStateEstablished = false;
|
||||
private boolean mEncryptedCall = false;
|
||||
private Listener<IMessage> mMessageListener;
|
||||
|
||||
/**
|
||||
* Constructs an instance
|
||||
@@ -111,7 +114,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
List<byte[]> frames = vm.getAMBEFrames();
|
||||
for(byte[] frame: frames)
|
||||
{
|
||||
processAudio(frame);
|
||||
processAudio(frame, message.getTimestamp());
|
||||
}
|
||||
}
|
||||
//Both Motorola and Hytera signal their Basic Privacy (BP) scrambling in some of the Voice B-F frames
|
||||
@@ -127,7 +130,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
List<byte[]> frames = voiceMessage.getAMBEFrames();
|
||||
for(byte[] frame: frames)
|
||||
{
|
||||
processAudio(frame);
|
||||
processAudio(frame, message.getTimestamp());
|
||||
}
|
||||
}
|
||||
else if(message instanceof VoiceMessage voiceMessage)
|
||||
@@ -135,7 +138,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
List<byte[]> frames = voiceMessage.getAMBEFrames();
|
||||
for(byte[] frame: frames)
|
||||
{
|
||||
processAudio(frame);
|
||||
processAudio(frame, message.getTimestamp());
|
||||
}
|
||||
}
|
||||
else if(!mEncryptedCallStateEstablished && message instanceof VoiceHeader voiceHeader)
|
||||
@@ -171,7 +174,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
* Processes the audio frame. Queues the frame until encryption state is determined. Once determined, the audio
|
||||
* frames are dequeued and audio is generated.
|
||||
*/
|
||||
private void processAudio(byte[] frame)
|
||||
private void processAudio(byte[] frame, long timestamp)
|
||||
{
|
||||
if(mEncryptedCallStateEstablished)
|
||||
{
|
||||
@@ -182,7 +185,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
{
|
||||
for(byte[] queuedFrame: mQueuedAmbeFrames)
|
||||
{
|
||||
produceAudio(queuedFrame);
|
||||
produceAudio(queuedFrame, timestamp);
|
||||
}
|
||||
|
||||
mQueuedAmbeFrames.clear();
|
||||
@@ -191,7 +194,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
mQueuedAmbeFrames.clear();
|
||||
}
|
||||
|
||||
produceAudio(frame);
|
||||
produceAudio(frame, timestamp);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -199,13 +202,13 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
}
|
||||
}
|
||||
|
||||
private void produceAudio(byte[] frame)
|
||||
private void produceAudio(byte[] frame, long timestamp)
|
||||
{
|
||||
try
|
||||
{
|
||||
IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(frame);
|
||||
addAudio(audioWithMetadata.getAudio());
|
||||
processMetadata(audioWithMetadata);
|
||||
processMetadata(audioWithMetadata, timestamp);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -217,7 +220,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
* Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the
|
||||
* tone metadata can be converted into a FROM identifier and included with any call segment.
|
||||
*/
|
||||
private void processMetadata(IAudioWithMetadata audioWithMetadata)
|
||||
private void processMetadata(IAudioWithMetadata audioWithMetadata, long timestamp)
|
||||
{
|
||||
if(audioWithMetadata.hasMetadata())
|
||||
{
|
||||
@@ -225,11 +228,11 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet())
|
||||
{
|
||||
//Each metadata map entry contains a tone-type (key) and tone (value)
|
||||
Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue());
|
||||
ToneIdentifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue());
|
||||
|
||||
if(metadataIdentifier != null)
|
||||
{
|
||||
broadcast(metadataIdentifier);
|
||||
broadcast(metadataIdentifier, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,13 +245,25 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
/**
|
||||
* Broadcasts the identifier to a registered listener
|
||||
*/
|
||||
private void broadcast(Identifier identifier)
|
||||
private void broadcast(ToneIdentifier identifier, long timestamp)
|
||||
{
|
||||
if(mIdentifierUpdateNotificationListener != null)
|
||||
{
|
||||
mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier,
|
||||
IdentifierUpdateNotification.Operation.ADD, getTimeslot()));
|
||||
}
|
||||
|
||||
if(mMessageListener != null)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("DMR Timeslot ");
|
||||
sb.append(getTimeslot());
|
||||
sb.append("Audio Tone Sequence Decoded: ");
|
||||
sb.append(identifier.toString());
|
||||
|
||||
mMessageListener.receive(new ToneIdentifierMessage(Protocol.DMR, getTimeslot(), timestamp,
|
||||
identifier, sb.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,6 +284,25 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
mIdentifierUpdateNotificationListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the listener to receive tone identifier messages from this module.
|
||||
* @param listener to receive messages
|
||||
*/
|
||||
@Override
|
||||
public void setMessageListener(Listener<IMessage> listener)
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the message listener
|
||||
*/
|
||||
@Override
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for squelch state to process end of call actions. At call end the encrypted call state established
|
||||
* flag is reset so that the encrypted audio state for the next call can be properly detected and we send an
|
||||
@@ -310,7 +344,7 @@ public class DMRAudioModule extends AmbeAudioModule implements IdentifierUpdateP
|
||||
* @param value of tone
|
||||
* @return an identifier with the accumulated tone metadata set
|
||||
*/
|
||||
public Identifier process(String type, String value)
|
||||
public ToneIdentifier process(String type, String value)
|
||||
{
|
||||
if(type == null || value == null)
|
||||
{
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.event;
|
||||
|
||||
@@ -30,13 +27,13 @@ import io.github.dsheirer.module.decode.event.filter.DecodeEventFilterSet;
|
||||
import io.github.dsheirer.module.decode.event.filter.EventFilterProvider;
|
||||
import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class DecodeEventModel extends AbstractTableModel implements Listener<IDecodeEvent>, EventFilterProvider<IDecodeEvent>
|
||||
{
|
||||
@@ -149,10 +146,12 @@ public class DecodeEventModel extends AbstractTableModel implements Listener<IDe
|
||||
*/
|
||||
public void receive(final IDecodeEvent event)
|
||||
{
|
||||
if (!mEventFilterSet.passes(event))
|
||||
{
|
||||
return;
|
||||
}
|
||||
//Disabled until #1368 decode event is fully implemented
|
||||
// if (!mEventFilterSet.passes(event))
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if(!mEvents.contains(event))
|
||||
{
|
||||
mEvents.add(0, event);
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2019 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.event;
|
||||
|
||||
@@ -44,18 +41,24 @@ import io.github.dsheirer.preference.PreferenceType;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.preference.swing.JTableColumnWidthMonitor;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import java.awt.*;
|
||||
import java.awt.Color;
|
||||
import java.awt.Component;
|
||||
import java.awt.EventQueue;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.ImageIcon;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
|
||||
public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain>
|
||||
{
|
||||
@@ -94,7 +97,8 @@ public class DecodeEventPanel extends JPanel implements Listener<ProcessingChain
|
||||
mTableColumnWidthMonitor = new JTableColumnWidthMonitor(mUserPreferences, mTable, TABLE_PREFERENCE_KEY);
|
||||
updateCellRenderers();
|
||||
|
||||
add(mEventManagementPanel, "span,growx");
|
||||
//Disabled until #1368 decode event is fully implemented
|
||||
// add(mEventManagementPanel, "span,growx");
|
||||
|
||||
mEmptyScroller = new JScrollPane(mTable);
|
||||
add(mEmptyScroller);
|
||||
|
||||
@@ -24,31 +24,33 @@ import io.github.dsheirer.audio.codec.mbe.AmbeAudioModule;
|
||||
import io.github.dsheirer.audio.squelch.SquelchState;
|
||||
import io.github.dsheirer.audio.squelch.SquelchStateEvent;
|
||||
import io.github.dsheirer.bits.BinaryMessage;
|
||||
import io.github.dsheirer.identifier.Identifier;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
|
||||
import io.github.dsheirer.identifier.IdentifierUpdateProvider;
|
||||
import io.github.dsheirer.identifier.Role;
|
||||
import io.github.dsheirer.identifier.tone.AmbeTone;
|
||||
import io.github.dsheirer.identifier.tone.P25ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.Tone;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifier;
|
||||
import io.github.dsheirer.identifier.tone.ToneIdentifierMessage;
|
||||
import io.github.dsheirer.identifier.tone.ToneSequence;
|
||||
import io.github.dsheirer.message.IMessage;
|
||||
import io.github.dsheirer.message.IMessageProvider;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.message.EncryptionSynchronizationSequence;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.message.mac.structure.PushToTalk;
|
||||
import io.github.dsheirer.module.decode.p25.phase2.timeslot.AbstractVoiceTimeslot;
|
||||
import io.github.dsheirer.preference.UserPreferences;
|
||||
import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import jmbe.iface.IAudioWithMetadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Queue;
|
||||
import jmbe.iface.IAudioWithMetadata;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdateProvider
|
||||
public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdateProvider, IMessageProvider
|
||||
{
|
||||
private final static Logger mLog = LoggerFactory.getLogger(P25P2AudioModule.class);
|
||||
|
||||
@@ -58,6 +60,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
private Queue<AbstractVoiceTimeslot> mQueuedAudioTimeslots = new ArrayDeque<>();
|
||||
private boolean mEncryptedCallStateEstablished = false;
|
||||
private boolean mEncryptedCall = false;
|
||||
private Listener<IMessage> mMessageListener;
|
||||
|
||||
public P25P2AudioModule(UserPreferences userPreferences, int timeslot, AliasList aliasList)
|
||||
{
|
||||
@@ -116,7 +119,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
{
|
||||
if(!mEncryptedCall)
|
||||
{
|
||||
processAudio(abstractVoiceTimeslot.getVoiceFrames());
|
||||
processAudio(abstractVoiceTimeslot.getVoiceFrames(), message.getTimestamp());
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -156,7 +159,12 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
}
|
||||
}
|
||||
|
||||
private void processAudio(List<BinaryMessage> voiceFrames)
|
||||
/**
|
||||
* Process the audio voice frames
|
||||
* @param voiceFrames to process
|
||||
* @param timestamp of the carrier message
|
||||
*/
|
||||
private void processAudio(List<BinaryMessage> voiceFrames, long timestamp)
|
||||
{
|
||||
if(hasAudioCodec())
|
||||
{
|
||||
@@ -168,7 +176,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
{
|
||||
IAudioWithMetadata audioWithMetadata = getAudioCodec().getAudioWithMetadata(voiceFrameBytes);
|
||||
addAudio(audioWithMetadata.getAudio());
|
||||
processMetadata(audioWithMetadata);
|
||||
processMetadata(audioWithMetadata, timestamp);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
@@ -182,7 +190,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
* Processes optional metadata that can be included with decoded audio (ie dtmf, tones, knox, etc.) so that the
|
||||
* tone metadata can be converted into a FROM identifier and included with any call segment.
|
||||
*/
|
||||
private void processMetadata(IAudioWithMetadata audioWithMetadata)
|
||||
private void processMetadata(IAudioWithMetadata audioWithMetadata, long timestamp)
|
||||
{
|
||||
if(audioWithMetadata.hasMetadata())
|
||||
{
|
||||
@@ -190,11 +198,11 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
for(Map.Entry<String,String> entry: audioWithMetadata.getMetadata().entrySet())
|
||||
{
|
||||
//Each metadata map entry contains a tone-type (key) and tone (value)
|
||||
Identifier metadataIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue());
|
||||
ToneIdentifier toneIdentifier = mToneMetadataProcessor.process(entry.getKey(), entry.getValue());
|
||||
|
||||
if(metadataIdentifier != null)
|
||||
if(toneIdentifier != null)
|
||||
{
|
||||
broadcast(metadataIdentifier);
|
||||
broadcast(toneIdentifier, timestamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,15 +213,28 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the identifier to a registered listener
|
||||
* Broadcasts the identifier to a registered listener and creates a new AMBE tone identifier message when tones are
|
||||
* present to send to the alias action manager
|
||||
*/
|
||||
private void broadcast(Identifier identifier)
|
||||
private void broadcast(ToneIdentifier identifier, long timestamp)
|
||||
{
|
||||
if(mIdentifierUpdateNotificationListener != null)
|
||||
{
|
||||
mIdentifierUpdateNotificationListener.receive(new IdentifierUpdateNotification(identifier,
|
||||
IdentifierUpdateNotification.Operation.ADD, getTimeslot()));
|
||||
}
|
||||
|
||||
if(mMessageListener != null)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("P25.2 Timeslot ");
|
||||
sb.append(getTimeslot());
|
||||
sb.append("Audio Tone Sequence Decoded: ");
|
||||
sb.append(identifier.toString());
|
||||
|
||||
mMessageListener.receive(new ToneIdentifierMessage(Protocol.APCO25_PHASE2, getTimeslot(), timestamp,
|
||||
identifier, sb.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -234,6 +255,25 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
mIdentifierUpdateNotificationListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a message listener to receive AMBE tone identifier messages.
|
||||
* @param listener to register
|
||||
*/
|
||||
@Override
|
||||
public void setMessageListener(Listener<IMessage> listener)
|
||||
{
|
||||
mMessageListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the message listener
|
||||
*/
|
||||
@Override
|
||||
public void removeMessageListener()
|
||||
{
|
||||
mMessageListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process AMBE audio frame tone metadata. Tracks the count of sequential frames containing tone metadata to
|
||||
* provide a list of each unique tone and a time duration (milliseconds) for the tone. Tones are concatenated into
|
||||
@@ -258,7 +298,7 @@ public class P25P2AudioModule extends AmbeAudioModule implements IdentifierUpdat
|
||||
* @param value of tone
|
||||
* @return an identifier with the accumulated tone metadata set
|
||||
*/
|
||||
public Identifier process(String type, String value)
|
||||
public ToneIdentifier process(String type, String value)
|
||||
{
|
||||
if(type == null || value == null)
|
||||
{
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25.phase1;
|
||||
|
||||
@@ -35,11 +32,10 @@ import io.github.dsheirer.dsp.psk.pll.PLLBandwidth;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class P25P1DecoderC4FM extends P25P1Decoder
|
||||
{
|
||||
@@ -110,7 +106,7 @@ public class P25P1DecoderC4FM extends P25P1Decoder
|
||||
//Process the buffer for power meter measurements (before gain is applied)
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,20 +49,20 @@ public class P25P1DecoderC4FMInstrumented extends P25P1DecoderC4FM
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
mMessageFramer.setCurrentTime(System.currentTimeMillis());
|
||||
mMessageFramer.setCurrentTime(samples.timestamp());
|
||||
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
float[] q = mQBasebandFilter.filter(samples.q());
|
||||
|
||||
if(mFilteredSymbolListener != null)
|
||||
{
|
||||
mFilteredSymbolListener.receive(new ComplexSamples(i, q));
|
||||
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
|
||||
}
|
||||
|
||||
//Process the buffer for power meter measurements (before gain is applied)
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25.phase1;
|
||||
|
||||
@@ -34,11 +31,10 @@ import io.github.dsheirer.dsp.window.WindowType;
|
||||
import io.github.dsheirer.module.decode.DecoderType;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class P25P1DecoderLSM extends P25P1Decoder
|
||||
{
|
||||
@@ -107,7 +103,7 @@ public class P25P1DecoderLSM extends P25P1Decoder
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
mMessageFramer.setCurrentTime(System.currentTimeMillis());
|
||||
mMessageFramer.setCurrentTime(samples.timestamp());
|
||||
|
||||
//The filter will decrement the user count when finished
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
@@ -116,7 +112,7 @@ public class P25P1DecoderLSM extends P25P1Decoder
|
||||
//Process the buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class P25P1DecoderLSMInstrumented extends P25P1DecoderLSM
|
||||
@Override
|
||||
public void receive(ComplexSamples samples)
|
||||
{
|
||||
mMessageFramer.setCurrentTime(System.currentTimeMillis());
|
||||
mMessageFramer.setCurrentTime(samples.timestamp());
|
||||
|
||||
//The filter will decrement the user count when finished
|
||||
float[] i = mIBasebandFilter.filter(samples.i());
|
||||
@@ -58,13 +58,13 @@ public class P25P1DecoderLSMInstrumented extends P25P1DecoderLSM
|
||||
|
||||
if(mFilteredSymbolListener != null)
|
||||
{
|
||||
mFilteredSymbolListener.receive(new ComplexSamples(i, q));
|
||||
mFilteredSymbolListener.receive(new ComplexSamples(i, q, samples.timestamp()));
|
||||
}
|
||||
|
||||
//Process the buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@ import io.github.dsheirer.protocol.Protocol;
|
||||
import io.github.dsheirer.record.AudioRecordingManager;
|
||||
import io.github.dsheirer.record.binary.BinaryReader;
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -63,6 +60,8 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.function.Consumer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* P25 Sync Detector and Message Framer. Includes capability to detect PLL out-of-phase lock errors
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
/*
|
||||
* *****************************************************************************
|
||||
* Copyright (C) 2014-2022 Dennis Sheirer
|
||||
*
|
||||
* * ******************************************************************************
|
||||
* * Copyright (C) 2014-2020 Dennis Sheirer
|
||||
* *
|
||||
* * This program is free software: you can redistribute it and/or modify
|
||||
* * it under the terms of the GNU General Public License as published by
|
||||
* * the Free Software Foundation, either version 3 of the License, or
|
||||
* * (at your option) any later version.
|
||||
* *
|
||||
* * This program is distributed in the hope that it will be useful,
|
||||
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* * GNU General Public License for more details.
|
||||
* *
|
||||
* * You should have received a copy of the GNU General Public License
|
||||
* * along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* * *****************************************************************************
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
* ****************************************************************************
|
||||
*/
|
||||
package io.github.dsheirer.module.decode.p25.phase2;
|
||||
|
||||
@@ -40,11 +37,10 @@ import io.github.dsheirer.module.decode.p25.phase2.enumeration.ScrambleParameter
|
||||
import io.github.dsheirer.sample.Listener;
|
||||
import io.github.dsheirer.sample.complex.ComplexSamples;
|
||||
import io.github.dsheirer.source.SourceEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* P25 Phase 2 HDQPSK 2-timeslot Decoder
|
||||
@@ -121,7 +117,7 @@ public class P25P2DecoderHDQPSK extends P25P2Decoder implements IdentifierUpdate
|
||||
//Process the buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ public class P25P2DecoderHDQPSKInstrumented extends P25P2DecoderHDQPSK
|
||||
//Process the buffer for power measurements
|
||||
mPowerMonitor.process(i, q);
|
||||
|
||||
ComplexSamples amplified = mAGC.process(i, q);
|
||||
ComplexSamples amplified = mAGC.process(i, q, samples.timestamp());
|
||||
mQPSKDemodulator.receive(amplified);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,24 @@ public class MP3Preference extends Preference
|
||||
{
|
||||
mInputAudioFormat = InputAudioFormat.getDefault();
|
||||
}
|
||||
|
||||
//Ensure the input sample rate is supported by the MP3 setting, or change it to the first supported rate.
|
||||
MP3Setting setting = getMP3Setting();
|
||||
|
||||
if(!setting.getSupportedSampleRates().contains(mInputAudioFormat))
|
||||
{
|
||||
InputAudioFormat supportedAudioFormat = InputAudioFormat.getDefault();
|
||||
|
||||
//Default should always be supported by all mp3 settings, but fall-back to a supported rate if not.
|
||||
if(!setting.getSupportedSampleRates().contains(supportedAudioFormat))
|
||||
{
|
||||
supportedAudioFormat = setting.getSupportedSampleRates().get(0);
|
||||
}
|
||||
|
||||
mLog.warn("MP3 Setting [" + setting + "] does not support input sample rate [" + getAudioSampleRate().name() +
|
||||
"] - updating to supported sample rate [" + supportedAudioFormat.name() + "]");
|
||||
setAudioSampleRate(supportedAudioFormat);
|
||||
}
|
||||
}
|
||||
|
||||
return mInputAudioFormat;
|
||||
|
||||
@@ -28,14 +28,14 @@ import io.github.dsheirer.source.SourceEvent;
|
||||
import io.github.dsheirer.util.Dispatcher;
|
||||
import io.github.dsheirer.util.ThreadPool;
|
||||
import io.github.dsheirer.util.TimeStamp;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.sound.sampled.AudioFormat;
|
||||
|
||||
/**
|
||||
* WAVE audio recorder module for recording complex (I&Q) samples to a wave file
|
||||
@@ -46,7 +46,7 @@ public class ComplexSamplesWaveRecorder extends Module implements IComplexSample
|
||||
private final static Logger mLog = LoggerFactory.getLogger(ComplexSamplesWaveRecorder.class);
|
||||
|
||||
private Dispatcher<ComplexSamples> mBufferProcessor = new Dispatcher<>(500,
|
||||
"sdrtrunk complex wave recorder", new ComplexSamples(new float[0], new float[0]));
|
||||
"sdrtrunk complex wave recorder", new ComplexSamples(new float[0], new float[0], System.currentTimeMillis()));
|
||||
|
||||
private AtomicBoolean mRunning = new AtomicBoolean();
|
||||
private BufferWaveWriter mWriter;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user