Compare commits

...

173 Commits

Author SHA1 Message Date
Denny
81b8f5a423 Updates build to version 0.4.0 alpha 4 2018-09-12 06:23:09 -04:00
Denny Sheirer
bf186a7408 Merge pull request #414 from DSheirer/405-airspy-tuner-waterfall-freeze
405 Airspy Spectrum/Waterfall Freezes
2018-09-12 06:20:36 -04:00
Denny
43fc326f93 Resolves #405
Adds 500 ms delay to initial registering for samples from all tuners to
feed the spectral display/waterfall.  The existing approach of immediately
registering for samples was causing the airspy tuner to lockup
aperiodically.  Investigation via debugger didn't show any application
thread locks ... usb master processor was continuing to service libusb
timeouts.  Unable to debug internal airspy processes.  Adding the delay
to startup seems to be a usable solution.
2018-09-12 06:19:11 -04:00
Denny Sheirer
7cdbb75b42 Merge pull request #413 from DSheirer/375-poor-p25-decode-performance
375 P25 Decode Performance
2018-09-10 04:22:40 -04:00
Denny
96a56cab68 Resolves #375.
Reverts PLL gain values to version 0.3.4beta2 values to restore P25
decoder performance.

This change in conjuction with recent channel sample rate change should
restore P25 channel decoding to comparable levels from 0.3.4b2.
2018-09-09 09:55:40 -04:00
Denny Sheirer
29d4b36cf7 Merge pull request #412 from DSheirer/411-variable-channel-sample-rates
#411 Update Tuner Source Manager to provide variable sample rate channels
2018-09-09 09:50:11 -04:00
Denny
b15a3e59a0 Resolves #411
Updates decoder types to specify channel sample rate and pass/stop
frequencies for requested channels.  Updates tuner channel source
managers to accept a channel specification for channels that are
allocated.
2018-09-09 09:49:05 -04:00
Denny Sheirer
9950e46e5a Merge pull request #408 from DSheirer/406-tuner-sample-delay-buffer
406 Tuner Sample Delay Buffer
2018-09-09 08:47:17 -04:00
Denny
570321da3e Implements a complex sample delay buffer integrated into the tuner
channel manager so that channel source requests can have a start
timestamp specified and the channel can be preloaded with samples from
the delay buffer.  If no timestamp is specified, current time is assumed.

This feature requires corresponding updates to any decoder that wishes
to implement the timestamped sample preloading functionality.
2018-09-09 08:46:21 -04:00
Denny
bb0bcd3198 Updates build to 0.4.0 alpha 3 2018-09-05 07:34:09 -04:00
Denny Sheirer
cb54850f99 Merge pull request #402 from DSheirer/heterodyne_channelizer
401 Update Tuners to use Heterodyne Channelizer
2018-09-05 07:31:57 -04:00
Denny
5bb8c46bd5 Merge remote-tracking branch 'origin/heterodyne_channelizer' into heterodyne_channelizer 2018-09-05 07:27:39 -04:00
Denny
2a3fd2c5a3 Class renaming and cleanup. 2018-09-05 07:27:18 -04:00
Denny
d712feb45a Class renaming and cleanup. 2018-09-05 07:26:52 -04:00
Denny
fbb24a4c05 Class renaming and cleanup. 2018-09-05 07:24:42 -04:00
Denny
3992f8178c Merge remote-tracking branch 'origin/heterodyne_channelizer' into heterodyne_channelizer
# Conflicts:
#	src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
2018-09-05 07:19:37 -04:00
Denny
8642def77e Cleanup, renaming and removing legacy code. 2018-09-05 07:17:51 -04:00
Denny
38e6de6cb2 Resolves cutoff frequency for 2 channel polyphase synthesizer. 2018-09-05 07:17:51 -04:00
Denny
994e78f42b Resolves issue with polyphase filter gain and channel output processor gain 2018-09-05 07:16:51 -04:00
Denny
b8a46af836 Creates new HeterodyneChannelSourceManager that uses frequency translation
and CIC decimation for each channel provided by the tuner.
2018-09-05 07:15:57 -04:00
Denny
1c6d19959a Class renaming and cleanup. 2018-09-05 06:34:13 -04:00
Denny
50bc3088b7 Removes unused legacy code. 2018-09-05 06:31:39 -04:00
Denny
09356848eb Merge branch 'master' into heterodyne_channelizer
# Conflicts:
#	src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
2018-09-05 06:18:27 -04:00
Denny Sheirer
316bfb19c2 Merge pull request #400 from DSheirer/375-channelizer-synthesizer-gain-issue
375 channelizer synthesizer gain issue
2018-09-05 06:17:47 -04:00
Denny
c045c8f16d Updates heterodyne channel manager to correctly lock/unlock the tuner
controller.
2018-09-05 06:11:28 -04:00
Denny
8946d789ff WIP - updated tuners to use CIC decimation tuner channel sources instead
of polyphase channelizer.

Channel sample rate reduced to 30 kHz - seems to work better with P25
decoders than previous 25 kHz.
2018-09-04 08:16:57 -04:00
Denny
19399d9138 Resolves cutoff frequency for 2 channel polyphase synthesizer. 2018-09-01 06:32:24 -04:00
Denny
9acdf22b91 WIP - identifier framework and restructuring P25 messages to provide
identifiers.
2018-08-31 04:50:58 -04:00
Denny Sheirer
f963d821a1 Merge pull request #396 from DSheirer/394-p25-bitstream-output
394 P25 bit stream output
2018-08-18 03:52:36 -04:00
Denny
ade0cef50b Updates P25 C4FM and LSM decoders to provide demodulated bit streams
for recording using the new binary recorders/format.
2018-08-18 03:36:17 -04:00
Denny
2044c2a2de WIP - Updated P25 C4FM and LSM decoders to assemble dibits and push
reusable byte buffers out to the processing chain.  This enables the
processing chain to broadcast these buffers back to the decoder for
message assembly and to any running binary recorder.

Updated P25Decoder to be a reusable byte buffer listener.  The decoder
will now pass reusable byte buffers down to the message framer for
message processing.  This should make the code more efficient since it
can process byte arrays versus handling each individual dibit.

The P25MessageFramer has been updated to be a listener of reusable byte
buffers instead of Dibits, but the actual processing of byte arrays
has to be coded.

Added max recording size to the binary recorder so that it cycles at
500 kB max file size.
2018-08-17 07:27:04 -04:00
Denny Sheirer
1c312a50b6 Merge pull request #395 from DSheirer/393-raw-bitstream-recording
393 Recording raw decoded bitstream from supported decoder types
2018-08-11 08:09:22 -04:00
Denny
54f6bd09e2 Cleanup 2018-08-11 08:08:57 -04:00
Denny
5f8f2ec330 Resolves #393 - Adds support for recording demodulated (raw) bitstream
buffers.  None of the decoders currently support this yet, but the P25
will be updated to provide bitstream output for recording.

Renames the ReusableBuffer class to ReusableFloatBuffer for
consistency.
2018-08-11 08:02:58 -04:00
Denny Sheirer
d7e018f66e Merge pull request #392 from DSheirer/390-polyphase-channel-source-npe
390 PolyphaseChannelSource NPE
2018-08-11 05:43:35 -04:00
Denny
c2a232fdee Adds null checking to output processor. 2018-08-11 05:42:45 -04:00
Denny Sheirer
c5d2dcd0cf Merge pull request #391 from DSheirer/389-add-active-to-channel-states
389 Add ACTIVE Channel State and Update P25 Decoder State processing
2018-08-11 05:39:20 -04:00
Denny
8e4e4129ca Adds 'ACTIVE' as a valid channel state to indicate that the channel is
actively decoding, but no call or data is being transmitted.  This is
primarily used for P25 channels when TDU Link Control messages are
being transmitted in between voice or data transmissions on a channel.

Updated several of the P25 decoder state message processing sections to
keep the channel state UI updated with current call identifiers and
states.

Hard-coded channel state colors to remove inefficiency in the way that
channel state coloration was being handled.
2018-08-11 05:36:30 -04:00
Denny
9176ca610f Updates build to Version 0.4.0 Alpha 2 2018-08-05 07:51:40 -04:00
Denny Sheirer
97b31e4d9b Merge pull request #385 from DSheirer/372-icecast-streaming-reconnect
372 Icecast Streaming Reconnect and Metadata Updates
2018-08-05 07:48:28 -04:00
Denny
a3e5520d3e Resolves #372 - Icecast reconnect following network outage. Updates
the lost connection timeout from default of 60 seconds down to 5 seconds
so that the connection state updates more quickly.  Removed complex
error handling and replaced with a simpler approach to catch an error
and disconnect and then attempt a reconnect.

Added new 'Network Unavailable' error state that detects when the network
connection is unavaialable.

Updated the icecast metadata updater to close the TCP session after
successfully writing metadata to the socket.
2018-08-05 07:44:46 -04:00
Denny
8c456972cc Decreases usable bandwidth of the R820T tuner to 98% so that channels
do not get allocated at the spectral extremes where the signal is
degraded.
2018-08-03 11:02:47 -04:00
Denny
35d81ab31d Decreases buffer size for low sample rates for RTL2832 tuners. 2018-08-03 08:06:00 -04:00
Denny
b90b06e600 Merge remote-tracking branch 'origin/master' 2018-08-03 07:57:58 -04:00
Denny
04144ca677 Adds additional RTL 2832 sample rate options 2018-08-03 07:57:47 -04:00
Denny Sheirer
d3c7a50918 Merge pull request #384 from DSheirer/382-rtl-tuner-sample-rate-options
382 - Additional Sample Rate Options for RTL2832 Tuners
2018-08-03 07:53:01 -04:00
Denny
0fd29fa35a Resolves #382 - Adds additional sample rate options for the RTL2832
tuner and adjusts the low sample rate buffer size down to 16 kb.
2018-08-03 07:50:36 -04:00
Denny Sheirer
c00b173064 Merge pull request #383 from DSheirer/381-rtl-tuner-adaptive-native-buffer-size
381 Adaptive USB Transfer Buffer Sizes for RTL2832 Tuners
2018-08-03 07:29:31 -04:00
Denny
2718367259 Resolves #381. Updates USB transfer processor to use native transfer
buffer sizes that adapt to the selected sample rate.
2018-08-03 07:27:58 -04:00
Denny Sheirer
3f0c13096a Merge pull request #380 from DSheirer/374-rtl-tuner-sample-rate-issue
374 RTL Tuner Sample Rates Not Supported (1.44 & 2.88 MHz)
2018-08-03 07:04:28 -04:00
Denny
71ba4adbd7 Resolves #374
Updates polyphase channel manager to correctly calculate the number of
channels for each sample rate change.
2018-08-03 07:02:58 -04:00
Denny Sheirer
3908e29a2e Merge pull request #376 from DSheirer/373-tuner-ppm-monitoring-and-adjustment
#373 - Tuner PPM Error Monitoring and R820T PPM Auto-Correction
2018-07-29 11:18:47 -04:00
Denny
0c47efea03 Resolves #373 - Adds support for Tuner frequency error PPM monitoring
and auto-correction for R820T tuners.

-Updated TunerModel and TunerViewPanel to add 2 new columns to
display the current tuner's PPM setting and (when available) the
measured frequency error averaged over 5 second intervals provided by
certain decoders (e.g. P25 C4FM and LSM).  Frequency error value from
P25 decoders is provided from the Costas Loop PLL once 2 or more P25
message sync events have been observed in a short period.
-Updated R820T tuner to auto-correct PPM when measured frequency error
exceeds the +/- 0.4PPM threshold for a period of 30 seconds or more.
Feature can be enabled/disabled by user in the tuner editor panel.

Resolved several issues with P25 C4FM/LSM decoders that was impacting
performance related to channel misalignment from tuner PPM offsets.
2018-07-29 10:48:29 -04:00
Denny
5c0d97d942 Resolves issue with array out of bounds exception on shutdown while
removing tuners from the tuner model.
2018-07-20 05:59:00 -04:00
Denny
61ae0d2d83 Resolves issue with saved setting for spectral display panel enabled state 2018-07-20 05:58:23 -04:00
Denny Sheirer
68f2a8da2c Merge pull request #371 from DSheirer/370-tuner-editor-lock-state
Resolved #370 Tuner Editor Lock State
2018-07-15 05:55:46 -04:00
Denny
0d69f11e65 Resolved #370 Tuner Editor Lock State
Resolves an issue with the previous implementation where the tuner
editor was being refreshed repeatedly to update the lock state for the
tuner, rendering the editor almost completely unusable.

Now updates only the frequency control and the sample rate control to
reflect the tuner's lock state.
2018-07-15 05:55:28 -04:00
Denny Sheirer
b3d5daea8b Merge pull request #369 from DSheirer/320-mixer-gain-r820t-tuner
360 R820T Mixer Gain Labels
2018-07-15 05:30:45 -04:00
Denny
fe34623c4a Resolves #320
Changes labels for R820T mixer gain settings.
2018-07-15 05:29:21 -04:00
Denny Sheirer
353747cecc Merge pull request #368 from DSheirer/96-disable-spectral-display-option
#96 Disable Spectral Display Option
2018-07-14 16:05:00 -04:00
Denny
a8c649ef64 Resolves #96
Adds capability to disable spectral display.

Adds right-click context
menu items to disable display and/or reenable display by selecting
another tuner.

Adds view menu items to disable spectral display or show a tuner.

Persists spectral display enabled state across application run sessions.
2018-07-14 16:04:18 -04:00
Denny Sheirer
8cf50e629e Merge pull request #367 from DSheirer/366-application-shutdown
366 Application Shutdown Procedure
2018-07-14 14:45:00 -04:00
Denny
20a8f2bb3c Fully implements application shutdown procedure that is invoked when the
user closes the the main gui window or invokes exit from the file menu.
2018-07-14 14:44:03 -04:00
Denny
8c237be682 Resolves issue with Mute/Unmute icon rendering. 2018-07-14 13:39:43 -04:00
Denny Sheirer
2d5271b849 Merge pull request #365 from DSheirer/364-p25-pll-lock-gain
#364 Excessive P25 PLL Sync Lock Timelines
2018-07-14 11:29:18 -04:00
Denny
609a1e86c7 Resolves #364.
The C4FM and LSM decoders were issuing a PLL reset with each new
channel that was allocated, causing any existing decoding channels to
be disrupted.  Decoding re-sync delays or inability to recapture the
 signal for decoding would likely result as the tuner's overall PPM
 caused misadjustment in the channel.

Resolved a NPE that was occurring in the FrequencyController for
sample rate changes.
2018-07-14 11:27:38 -04:00
Denny Sheirer
821e719015 Merge pull request #363 from DSheirer/170-airspy-mini-sample-rates
#170 Add Support for Airspy Mini tuner's 3 and 6 MHz
2018-07-14 07:47:03 -04:00
Denny
104c4f72be Resolves #170 adding support for Airspy Mini tuner's 3 and 6 MHz
sample rates.

Resolves issue in PolyphaseChannelManager responding to sample rate
changes in the tuner.  The manager now recreates the channelizer when
needed if the tuner's sample rate has changed since the last time the
channelizer was used.

Updates the Tuner selection view to reflect tuner lock state.  Updates
the tuner editors to enable/disable frequency controller and sample
rate controls according to the tuner's lock state.
2018-07-14 07:45:42 -04:00
Denny
9e7fa7618d Logging cleanup. 2018-07-04 06:05:15 -04:00
Denny
ee09520181 Updates build script to version 0.4.0 alpha 1. Adds missing resampler
library.
2018-07-01 20:52:38 -04:00
Denny Sheirer
fa12b840db Merge pull request #357 from DSheirer/39-polyphase-channelizer
39 polyphase channelizer and decoder updates
2018-07-01 07:30:06 -04:00
Denny
183d7d6d19 Logging cleanup 2018-07-01 07:25:37 -04:00
Denny
e2550869ce Disables debug logging of reusable buffer creation. 2018-07-01 07:21:42 -04:00
Denny
e88944e53d Updates all reusable buffer queues to automatically set the user count
to 1 on all provided buffers by default.
2018-07-01 07:08:52 -04:00
Denny
0051a1d381 Removes debug logging for polyphase channelizer and reusable buffer queues. 2018-07-01 06:43:40 -04:00
Denny
f7a873dd0c Removes AFC settings from all decode configurations. 2018-07-01 05:43:42 -04:00
Denny
ad560db5ea [WIP] Removes all usage of Buffer, RealBuffer and ComplexBuffer. 2018-06-30 06:54:36 -04:00
Denny
6f1ce79461 [WIP] Updates the AM decoder module.
Removes a number of unused classes in an effort to remove RealBuffer
usage across the codebase (has been replaced by ReusableBuffers).
2018-06-30 05:37:13 -04:00
Denny
df1bc1031e [WIP] Updates AudioPackets to be ReusableAudioPackets. 2018-06-29 06:53:03 -04:00
Denny
a621a31aa9 [WIP] Updates the sample adapters to natively use ReusableBuffers.
Refactors a MixerReader instance from the RealMixerSource so that it
can be reused for both real and complex data outputs.

Updates RealMixerSource to use refactored MixerReader and use a default
sample rate of 8 kHz.

Updates the ComplexMixer to use the refactored MixerReader.

Updates the FCD 1/2 tuners for compatibility with the new ComplexMixer.

Removes several unused classes.
2018-06-24 09:50:36 -04:00
Denny
19fa256c60 [WIP]Updates mixer manager and mixer source to use 8 kHz audio for
sound card inputs.

Updated DecoderFactory to only include an AM/FM demodulator when we're
using a tuner input.
2018-06-23 09:49:40 -04:00
Denny
7a830077bd [WIP] Cleanup. Refactors DecoderFX application to the gui package. 2018-06-15 07:29:27 -04:00
Denny
f4793f3cfd [WIP] Cleanup. Removes the legacy Instrumentable, Taps, etc and related
gui components.  Refactors the new DecoderFX application to the
instrument package.
2018-06-15 07:27:17 -04:00
Denny
2cd3b73633 [WIP]Updates the MDC1200 decoder to use the new AFSK1200 decoder
class.

Adds Demodulator Viewer FX pane for MDC1200 decoder.
2018-06-15 07:11:07 -04:00
Denny
18f50485d3 [WIP]Updates the Tait1200 decoder to use the new AFSK1200 decoder
class.

Adds Demodulator Viewer FX pane for Tait1200 decoder.
2018-06-15 06:56:44 -04:00
Denny
b410cf0dfe [WIP]Updates the LJ-1200 decoder to use the new AFSK1200 decoder
class.

Adds Demodulator Viewer FX panes for MPT-1327 and LJ-1200 decoders.
2018-06-15 06:19:50 -04:00
Denny
1fdcb623a6 [WIP]Updates the MPT-1327 decoder to use the new AFSK1200 decoder
class.
2018-06-11 04:32:30 -04:00
Denny
9dda1f95a1 [WIP] Creates a new AFSK-1200 correlation decoder and updates the
Fleetsync II decoder to use it.  Adds instrumented version of the decoder
and a new Fleetsync2 FX pane to the DemodulatorViewerFX tool.

Note: the remaining AFSK-1200 based decoders will all be ported to use
this new AFSK1200 decoder (MDC1200, Tait1200, LowJack1200).
2018-06-09 08:17:18 -04:00
Denny
a5c8552937 [WIP] new AFSK1200 decoder. 2018-06-09 04:38:47 -04:00
Denny
e4209c8e4f [WIP] Updates the passport decoder to use the new LTRDecoder.
Deprecates the Listener<Boolean> interface method in the MessageFramer
to optimize for primitive booleans and reduce impact on memory.  Will
remove this interface once the AFSK1200 decoders are updated.
2018-06-02 09:31:49 -04:00
Denny
a898be265f [WIP] Updates the LTR-Standard and LTR-Net decoders to use a new LTR
decoder with a zero-crossing symbol timing error detector.  The new
decoder is optimized for 8 kHz FM demodulated audio produced by the
AudioModule.
2018-06-02 08:47:48 -04:00
Denny
c13315f731 [WIP] The Two-Channel synthesizer is now fully-functional and the
channelizer sub-system is functionally complete.

Updated the channelizer and synthesizer filter design methods to use a
Kaiser windowed Nyquist filter.  Updated the synthesizer filter design
to use a new cutoff frequency of 105% of the channel bandwidth to
resolve a slight dip in the synthesized channel gain at the channel
union region.

Updated the 2 channel output processor to use a low-pass filter that
is designed for the runtime channel sample rate versus hard-coded 12.5
khz.

Testing and redesign continues ...
2018-05-20 07:21:14 -04:00
Denny
341e740e77 [WIP] updated the 2 channel synthesizer to use a more SIMD friendly
filter structure.  Updated the channel results array buffer to produce
reusable buffers.

This has dropped the overall memory usage considerably when profiling.

Resolved issue in the processing chain where polyphase channel sources
were not being stopped and were stacking up in memory.
2018-05-10 08:08:20 -04:00
Denny
cbc228d798 [WIP] The 2 channel synthesizer appears to be working after restructuring.
Check-point commit.  I'm going to optimize the code so that it can
leverage SIMD instructions.
2018-05-06 07:37:24 -04:00
Denny
488b2e02ce [WIP] Work continues on resolving issues with the 2-channel synthesizer
that is currently up/down converting the signal of interest, but the
synthesizer is not correctly combining the original two channels.

Some experimentation with the tap count for the channelizer indicates
that we can cut the tap count in half and still have decent decoding
on the P25 decoder when paired to an aligned channelized output.  The
2-channel synthesized output decodes partially, but needs more work.

Resolved issue with channel gain in the channelizer that was making the
sample values so small that they were registering as zero when converted
from float to 16-bit int value in the complex wave recorder.
2018-04-29 06:19:39 -04:00
Denny
e395d737f8 [WIP]-39 updates to resolve issue with stopping channel streams. 2018-04-22 04:44:35 -04:00
Denny
7a9f705517 Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	src/main/java/io/github/dsheirer/module/decode/p25/P25DecoderC4FM.java
2018-04-18 06:00:57 -04:00
Denny
dfaf506e23 Updates changelog 2018-04-18 05:52:15 -04:00
Denny
81c9a352a8 Updates changelog 2018-04-18 05:51:55 -04:00
Denny
e6937764c3 Merge remote-tracking branch 'origin/master' 2018-04-18 05:49:16 -04:00
Denny
5bfc5bf456 Increments build to 0.3.4 beta 2 2018-04-18 05:49:04 -04:00
Denny Sheirer
ce83273393 Merge pull request #345 from DSheirer/344-p25-c4fm-agc
344 Add AGC to P25 C4FM decoder
2018-04-18 09:48:19 +00:00
Denny
95aa9f8803 Adds automatic gain control to P25 C4FM decoder to condition the
samples prior to interpolation.

Resolves issue with using incorrect quadrature index when pulling a
sample from the interpolating buffer.

Resolves issue where the instrumented version of the interpolating
buffer was being constructed in the non-interpolated decoder component.
2018-04-18 05:47:17 -04:00
Denny
e971ad4582 39 [WIP] - restructured the polyphase filter process. Previous version
was not computationally feasible with my 3rd Gen i7 processor running
and airspy at 10 MHz.  Restructured the polyphase channelizer to use
vector iteration structures that the JDK 9 JIT compiler can translate
into SIMD instructions.  Separated the IFFT processing from the filter
process so that they can be executed on separate threads and this allows
the processing to occur real time.

P25 C4FM decoding at 25 kHz channel sample rate using an airspy
10MHz sample stream seems to be working nicely with ~25% CPU usage and
~300 to 500 MB memory usage.  Incremental (parallel) channel decoding
should have a minimal increase in CPU loading since all of the 12.5 kHz
channels are available in the polyphase channelizer and the only
additional workload is the decoding of the 25 kHz sample stream.

There is still an issue with stopping channels and the channelizer keeps
 processing, and occasionally when starting traffic channels something
 deadlocks and causes out of memory.
2018-04-15 09:05:06 -04:00
Denny
ff9395e4b9 39 [WIP] working copy with new SIMD structures in place. 2018-04-14 07:23:33 -04:00
Denny
d023da9ead 39 [WIP] experiments to reduce processing time for high sample rate tuners (e.g airspy) 2018-03-26 09:47:32 -04:00
Denny
60ffb18cd9 39 [WIP] cleans up the reusable buffers and creates both real and
complex variants.
2018-03-25 09:13:03 -04:00
Denny
c36f6901af 39 - Adds P25/LSM support to the DemodulatorViewerFX application and
adds Feed forward gain support to the C4FM DQPSK demodulator which seems
to improve output from the interpolating sample buffer at each symbol
period.  Updates the symbol timing gain for C4FM to decrease the gain a
bit since the feed forward gain is providing a better interpolated symbol.
2018-03-24 11:03:18 -04:00
Denny
bfaacb309d 39 - WIP - incorporates the latest P25 C4FM decoder from master and
updates the LSM decoder to follow the same design pattern.  Adds LSM
decoder support to the demodulator viewer application.  The current
symbol timing gain for LSM is set too high -- is using the C4FM gain
and causes too much fluctuation in the timing error.
2018-03-24 06:59:41 -04:00
Denny
c3b405a752 Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	src/main/java/io/github/dsheirer/buffer/FloatCircularBuffer.java
#	src/main/java/io/github/dsheirer/dsp/filter/FilterFactory.java
#	src/main/java/io/github/dsheirer/dsp/filter/design/FilterView.java
#	src/main/java/io/github/dsheirer/dsp/filter/design/FilterViewer.java
#	src/main/java/io/github/dsheirer/dsp/filter/fir/complex/ComplexFIRFilter.java
#	src/main/java/io/github/dsheirer/dsp/gain/ComplexFeedForwardGainControl.java
#	src/main/java/io/github/dsheirer/dsp/mixer/Oscillator.java
#	src/main/java/io/github/dsheirer/dsp/psk/LSMDemodulator.java
#	src/main/java/io/github/dsheirer/module/decode/Decoder.java
#	src/main/java/io/github/dsheirer/module/decode/DecoderFactory.java
#	src/main/java/io/github/dsheirer/module/decode/p25/P25Decoder.java
#	src/main/java/io/github/dsheirer/module/decode/p25/P25_C4FMDecoder.java
#	src/main/java/io/github/dsheirer/module/decode/p25/P25_LSMDecoder.java
#	src/main/java/io/github/dsheirer/source/tuner/TunerChannelSource.java
#	src/main/java/io/github/dsheirer/source/tuner/TunerModel.java
#	src/main/java/io/github/dsheirer/source/tuner/TunerSourceEditor.java
#	src/main/java/io/github/dsheirer/spectrum/ChannelSpectrumPanel.java
2018-03-18 05:19:33 -04:00
Denny
28da8b2fa4 [wip] reusable buffer support. 2018-02-09 05:56:20 -05:00
Denny
28cb9d9407 [WIP] Updating dsp modules to accept variable sample rate I/Q streams
and creating a new audio resampler.
2018-02-09 05:29:07 -05:00
Denny
96d9d6f750 [WIP] Adds locking mechanism to tuner controllers so that the
polyphase channel manager can block users from changing frequency and/or
sample rate when polyphase channels are allocated.
2018-02-04 06:30:25 -05:00
Denny
85691edacb [WIP] Testing completed for channel manager allocating tuner channels
when the tuner controller indicates that there is a center DC spike
blocked region or when the tuner has clipped outer spectral regions
resulting in less than 100% usable bandwidth.
2018-02-04 04:59:27 -05:00
Denny
f2f2c92afe [WIP] Implements reusable sample buffers across all components. This
produces a significant reduction in memory usage, especially for tuners
like the airspy and hackrf that produce 10/20 MSPS sample rates.  This
is observed to be around 66% reduction in peak memory usage and provides
a more stable memory utilization across time reducing stress on the
garbage collector.

Use of these reusable memory buffers will be pushed further down into
the processing chain modules to ensure we keep new object creation
to the lowest levels possible.
2018-02-03 10:05:16 -05:00
Denny
fcd3c2fd46 [WIP] channelizer and 1/2 channel output processors along with the 2
channel synthesizer ... all appear to be functional.
2018-01-28 10:52:53 -05:00
Denny
e78d2339ff WIP - aligns the SynthesizerViewer configuration with that of the
ChannelizerViewer so that we can compare the two-channel synthesis
operations.  The SynthesizerViewer is successfully down-sampling and
synthesizing two 25.0 kHz channels into a combined 25.0 kHz channel with
no noticeable drop/nulling in the joined region.  However, the channelizer
viewer's synthesized channels definitely have a null in the joined
region.  I suspect that the channelizer is not quite where it needs to
be and am going to start experimenting there.
2018-01-27 16:27:58 -05:00
Denny
673c8a2423 WIP - two-channel output processor is correctly synthesizing a channel
and translating it by -FS/4 to correctly center the synthesized channel.
The low-pass filter is currently disabled to better troubleshoot/view the
state of the synthesized channel.  It appears that the synthesizer is
not correctly joining the channel ... instead of down-sampling and then
synthesizing the two channels, it appears to be synthesizing and then
down-sampling.  The synthesized channel has a null in the middle which
would render it unusable.
2018-01-27 15:57:22 -05:00
Denny
5a03a0038c WIP - Adds FS/4 up and down converter classes. 2018-01-27 15:37:26 -05:00
Denny
e7f7d27a1f Updates the Oscillator and LowPhaseNoiseOscillator classes to share a
common interface and abstract class.
2018-01-27 04:41:01 -05:00
Denny
0cf964fa98 Updates test tuner to cleanup the buffer reuse settings 2018-01-20 04:26:53 -05:00
Denny
b6f282a85a Polyphase Channelizer Viewer is fully functional again, now using the
TestTuner and TestTunerController implementations.  This implementation
only has support for channels that are completely centered within a
channelizer output.  Two output slice stitching to form a composite
channel will be implemented next.

Initial profiling indicates that the FFT function within the polyphase
channelizer is now the dominant processing workload - 28% cpu loading
when channelizing a 10 MHz sample rate input into 800 channels running
at 25.0 kHz each.  This is ideal for offloading to an OpenCL kernel and
running on a GPU.
2018-01-19 12:44:31 -05:00
Denny
32ab0598e8 Updates sample generator and test tuner controller to support multiple
complex buffer listeners.  Removes complex buffer broadcast support from
the source manager and renames it to channel source manager to convey
that it's only responsible for providing channels from the tuner.
2018-01-15 05:34:36 -05:00
Denny
5ae3cb38e1 Merge master and re-apply the source event structures to the DFT processor. 2018-01-15 05:03:03 -05:00
Denny
78205bb1bf Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	src/main/java/io/github/dsheirer/spectrum/DFTProcessor.java
2018-01-15 04:56:16 -05:00
Denny
a7a0c26b38 #39 WIP - changes for testing of the channelizer viewer. 2018-01-14 08:40:34 -05:00
Denny
d4761be00a #39 WIP - rolls back embedding source events in buffers. The channel
calculator must be updated immediately any time the tuner's center
frequency changes so that a channel can be allocated immediately.  So,
I changed source event handling to two parts where the channel calculator
is immediately updated and the channelizer configuration is deferred so
that it can happen on the buffer processor execution thread and we can
avoid any sync() locking on the channelizer or polyphase channel
sources.
2018-01-13 11:43:26 -05:00
Denny
05fa5c25fd #39 WIP - adds support for timestamped complex buffers so that decoded
messages and events can be accurately timestamped, and more importantly,
allocated traffic channels can request to be pre-loaded with buffered
samples to ensure that traffic channels don't miss any samples that
are currently missed due processing latency inherent in current buffer
sizing.  This should reduce the clipping of starting messages or voice
on the traffic channels.

Restructures processing path for tuner frequency and sample rate change
events.  The tuner now embeds these events in the sequence of buffers
that are generated immediately after the event occurs since those are
the buffers that will be impacted by the change.  Previously, the change
event would be broadcast throughout the system and would impact any
buffered/cached buffers.  This also reduces the number of source event
change listeners that have to be managed.  Note: this change is not yet
implemented in all tuners ... only in the TestTuner implementation.

Updates the polyphase channelizer to now buffer channel results and then
dispatch the buffer to the channel output processors to improve efficiency
of using float arrays versus single value complex float processing.
2018-01-13 09:10:57 -05:00
Denny
d2e7f008c8 WIP - implements tuner source manager to calculate and update the tuner's
center frequency each time we allocate a new DDC/channel.  Adds
 support for broadcasting channel count change source events so that the
 tuner can accurately convey sourced channel counts via tuner events.

 Completes changeover of polyphase channel manager to support arbitrary
 bandwidth channels.  The new approach provides greater compatibility
 across any tuner sample rates.  Likewise, all decoders will have to be
 updated to adapt to any channel sample rate, although channel bandwidths
 will be at least 12.5 kHz and likely no more than 25.0 kHz, with a 2x
 oversampling that produces channel sample rates in range 25.0 - 50.0
 kHz.
2018-01-07 07:46:41 -05:00
Denny
236df8f3f4 Merges from master branch. 2017-12-27 10:10:51 -05:00
Denny
667c50174d Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	.idea/dictionaries/denny.xml
#	.idea/libraries/imports.xml
#	sdrtrunk.iml
#	src/audio/AudioModule.java
#	src/audio/broadcast/StreamManager.java
#	src/buffer/FloatCircularBuffer.java
#	src/channel/state/AlwaysUnsquelchedDecoderState.java
#	src/channel/state/ChannelState.java
#	src/channel/state/DecoderState.java
#	src/channel/traffic/TrafficChannelManager.java
#	src/controller/channel/Channel.java
#	src/dsp/filter/FilterFactory.java
#	src/dsp/filter/Window.java
#	src/dsp/filter/design/FilterView.java
#	src/dsp/filter/design/FilterViewer.java
#	src/dsp/filter/fir/FIRFilterSpecification.java
#	src/dsp/filter/fir/remez/RemezFIRFilterDesigner.java
#	src/dsp/gain/AutomaticGainControl.java
#	src/dsp/gain/ComplexFeedForwardGainControl.java
#	src/dsp/mixer/Oscillator.java
#	src/dsp/psk/LSMDemodulator.java
#	src/gui/control/JFrequencyControl.java
#	src/instrument/tap/stream/BinaryTapViewPanel.java
#	src/module/Module.java
#	src/module/ProcessingChain.java
#	src/module/decode/Decoder.java
#	src/module/decode/am/AMDecoder.java
#	src/module/decode/fleetsync2/Fleetsync2Decoder.java
#	src/module/decode/fleetsync2/Fleetsync2DecoderState.java
#	src/module/decode/lj1200/LJ1200Decoder.java
#	src/module/decode/lj1200/LJ1200DecoderState.java
#	src/module/decode/ltrnet/LTRNetDecoder.java
#	src/module/decode/ltrnet/LTRNetDecoderState.java
#	src/module/decode/ltrstandard/LTRStandardDecoder.java
#	src/module/decode/ltrstandard/LTRStandardDecoderState.java
#	src/module/decode/mdc1200/MDCDecoder.java
#	src/module/decode/mdc1200/MDCDecoderState.java
#	src/module/decode/mpt1327/MPT1327Decoder.java
#	src/module/decode/mpt1327/MPT1327DecoderState.java
#	src/module/decode/nbfm/NBFMDecoder.java
#	src/module/decode/p25/C4FMSymbolFilter.java
#	src/module/decode/p25/P25Decoder.java
#	src/module/decode/p25/P25DecoderState.java
#	src/module/decode/p25/P25_C4FMDecoder.java
#	src/module/decode/p25/P25_LSMDecoder.java
#	src/module/decode/p25/audio/P25AudioModule.java
#	src/module/decode/passport/PassportDecoder.java
#	src/module/decode/passport/PassportDecoderState.java
#	src/module/decode/tait/Tait1200Decoder.java
#	src/module/decode/tait/Tait1200DecoderState.java
#	src/module/demodulate/am/AMDemodulatorModule.java
#	src/module/demodulate/audio/DemodulatedAudioFilterModule.java
#	src/module/demodulate/fm/FMDemodulatorModule.java
#	src/module/log/EventLogger.java
#	src/record/AudioRecorder.java
#	src/record/RecorderManager.java
#	src/record/wave/ComplexBufferWaveRecorder.java
#	src/record/wave/RealBufferWaveRecorder.java
#	src/sample/Broadcaster.java
#	src/sample/OverflowableTransferQueue.java
#	src/sample/SampleType.java
#	src/source/ComplexSource.java
#	src/source/ISourceEventProvider.java
#	src/source/InvalidFrequencyException.java
#	src/source/RealSource.java
#	src/source/Source.java
#	src/source/config/SourceConfigRecording.java
#	src/source/config/SourceConfigTuner.java
#	src/source/heartbeat/Heartbeat.java
#	src/source/heartbeat/IHeartbeatListener.java
#	src/source/heartbeat/IHeartbeatProvider.java
#	src/source/mixer/RealMixerSource.java
#	src/source/recording/Recording.java
#	src/source/recording/RecordingSourceManager.java
#	src/source/tuner/ITunerChannelProvider.java
#	src/source/tuner/Tuner.java
#	src/source/tuner/TunerClass.java
#	src/source/tuner/TunerController.java
#	src/source/tuner/TunerEditor.java
#	src/source/tuner/TunerManager.java
#	src/source/tuner/TunerModel.java
#	src/source/tuner/TunerSourceEditor.java
#	src/source/tuner/TunerType.java
#	src/source/tuner/airspy/AirspyTuner.java
#	src/source/tuner/airspy/AirspyTunerController.java
#	src/source/tuner/airspy/AirspyTunerEditor.java
#	src/source/tuner/configuration/TunerConfigurationModel.java
#	src/source/tuner/fcd/FCDTuner.java
#	src/source/tuner/fcd/FCDTunerController.java
#	src/source/tuner/fcd/proV1/FCD1TunerController.java
#	src/source/tuner/fcd/proV1/FCD1TunerEditor.java
#	src/source/tuner/fcd/proplusV2/FCD2TunerController.java
#	src/source/tuner/fcd/proplusV2/FCD2TunerEditor.java
#	src/source/tuner/hackrf/HackRFTuner.java
#	src/source/tuner/hackrf/HackRFTunerController.java
#	src/source/tuner/hackrf/HackRFTunerEditor.java
#	src/source/tuner/rtl/RTL2832Tuner.java
#	src/source/tuner/rtl/RTL2832TunerController.java
#	src/source/tuner/rtl/e4k/E4KTunerController.java
#	src/source/tuner/rtl/e4k/E4KTunerEditor.java
#	src/source/tuner/rtl/r820t/R820TTunerController.java
#	src/source/tuner/rtl/r820t/R820TTunerEditor.java
#	src/source/tuner/usb/USBTransferProcessor.java
#	src/source/wave/ComplexWaveSource.java
#	src/source/wave/RealWaveSource.java
#	src/spectrum/ChannelSpectrumPanel.java
#	src/spectrum/DFTProcessor.java
#	src/spectrum/OverlayPanel.java
#	src/spectrum/SpectralDisplayPanel.java
2017-12-26 08:35:33 -05:00
Denny
1c71a37755 #39 WIP - 2017-12-11 20:30:19 -05:00
Denny
318e6f3586 #39 WIP - changes tuner controllers to expose the frequency controller
and the channel manager in preparation for polyphase.
2017-12-11 20:27:01 -05:00
Denny
f38184fd83 #39 WIP - finishes polyphase channel source processing of frequency
offset calculation and updating for channel frequency correction.
2017-12-10 06:26:44 -05:00
Denny
d2085052f6 #39 WIP - updates tunerchannelsource as the parent class for the CIC
and polyphase sub class implementations.  Converts all source implementations
and tuners to treat sample rate as a double.
2017-12-09 15:23:40 -05:00
Denny
441b595325 #39 WIP - initial efforts to restructure the way tuners and tuner
controllers manager sample buffer processing and channel management.
General cleanup across all modules to remove the executor from each
of the start() methods.  Removed the B100 tuner class since it's not
yet implemented.
2017-12-08 04:02:49 -05:00
Denny
57fab2b7ca #39 WIP - initial efforts to restructure the way tuners and tuner
controllers manager sample buffer processing and channel management.
General cleanup across all modules to remove the executor from each
of the start() methods.  Removed the B100 tuner class since it's not
yet implemented.
2017-12-03 11:10:50 -05:00
Denny
bf65e10286 #39 WIP polyphase channelizer
Updates the M2 channelizer and correctly implements the ChannelCalculator
2017-12-02 08:58:34 -05:00
Denny
a12aaf7cf3 #39 WIP polyphase channelizers. 2017-11-18 07:33:28 -05:00
Denny
b3e4bd6a9c #39 WIP polyphase channelizers. 2017-11-18 05:50:00 -05:00
Denny
5281679b7f #39 WIP polyphase channelizers. Creates test tuner and controller to
use in testing tuner and polyphase channel manager operations.
2017-11-12 10:33:13 -05:00
Denny
a581da56a8 #39 WIP polyphase channelizers. 2017-11-05 11:38:01 -05:00
Denny
0181926fe3 WIP - polyphase channelizer 2017-10-30 19:12:14 -04:00
Denny
2c3be02939 WIP - polyphase channelizer 2017-10-23 08:43:53 -04:00
Denny
a00ec42230 Cleanup from master merge 2017-10-22 06:37:24 -04:00
Denny
14a3532030 Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	src/source/tuner/TunerController.java
2017-10-22 06:28:57 -04:00
Denny
3be65d5e9a Resolves issue from merging master branch into polyphase channelizer branch. 2017-10-15 07:26:17 -04:00
Denny
c21f3fca23 Resolves issue from merging master onto branch. 2017-10-15 07:19:36 -04:00
Denny
99d81d247a Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	src/channel/state/ChannelState.java
#	src/module/ProcessingChain.java
#	src/source/Source.java
#	src/source/mixer/RealMixerSource.java
#	src/source/tuner/TunerChannelSource.java
#	src/source/wave/RealWaveSource.java
2017-10-15 07:17:57 -04:00
Denny
27461df7f0 WIP on 2 channel output processor 2017-09-18 04:15:04 -04:00
Denny
9983d2119d WIP 2017-09-10 04:42:35 -04:00
Denny
9e275b7d62 WIP 2017-09-02 06:55:58 -04:00
Denny
f461ed9cdb wip-channelizer 2017-05-13 10:16:18 -04:00
Denny
7c6b7494b8 wip-channelizer 2017-04-21 12:17:55 -04:00
Denny
ab31b40124 Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	src/spectrum/SpectrumPanel.java
2017-04-14 14:42:56 -04:00
Denny
ac686f5f64 WIP - correctly implements the 2x oversampled polyphase channelizer. 2017-04-14 06:41:43 -04:00
Denny
f7cd798f07 WIP 2017-04-08 08:35:48 -04:00
Denny
41d307cdad WIP - checkpointing update for 2x oversampling where we're only computing half of the coefficients. 2017-04-03 19:04:00 -04:00
Denny
afae1654d5 WIP 2017-04-01 15:10:02 -04:00
Denny
ffc92a8f0e WIP - adds remez channelizer builder, working toward combined support (remez & sync) via the filter factory. 2017-03-30 19:16:37 -04:00
Denny
6d9e129087 WIP - adds windowed-sinc (nyquist) filter designer and polyphase channelizer filter designer to FilterFactory. Resolves a few issues with Window designers. ChannelizerViewer is now able to show 800 channels with 12.5 kHz channelization of a 10 MHz bandwidth input signal. 2017-03-26 10:23:38 -04:00
Denny
0e1da8fb3b Updates intellij files and includes missing library 2017-03-23 11:31:15 +00:00
Denny
7d5371db4c Updates build to 0.3.0 Beta 9 2017-03-20 06:12:15 -04:00
Denny
131290fcd2 WIP - experiments 2017-03-11 06:08:23 -05:00
Denny
e2fe06638b Incrementing build to version 0.3.0 beta8 2017-03-05 05:55:04 -05:00
Denny
17c16e16ee Adds a new remez designer that uses BigDecimal to handle the larger values inherent in longer length filters. However, this designer is hugely inefficient and takes forever to calculate the filter. More experimentation needed. 2017-03-02 19:17:22 -05:00
Denny
cd4eaedfcc Merge branch 'master' into 39-polyphase-channelizer 2017-02-26 13:35:33 -05:00
Denny
fd748fec79 [WIP] Using different filters 2017-02-20 20:16:38 -05:00
Denny
14b844f0bd [WIP] Channelizing 6.4 MHz sample rate into 512 channels with with 512-point FFT on each channel at frame rate of 20 fps. 2017-02-19 10:37:44 -05:00
Denny
99ef002d07 [WIP] working on polyphase channelizer and testing viewer 2017-02-19 08:22:37 -05:00
Denny
e868428c88 [WIP] working on polyphase channelizer and testing viewer 2017-02-18 11:05:21 -05:00
Denny
d2542d2560 Merge branch 'master' into 39-polyphase-channelizer
# Conflicts:
#	src/dsp/filter/design/FilterViewer.java
2017-02-18 07:34:18 -05:00
Denny
9ca87299d5 [WIP] Polyphase channelizer experiments 2017-02-06 04:19:09 -05:00
472 changed files with 35774 additions and 26180 deletions

32
.idea/dictionaries/denny.xml generated Normal file
View File

@@ -0,0 +1,32 @@
<component name="ProjectDictionaryState">
<dictionary name="denny">
<words>
<w>bahroo</w>
<w>broadcastify</w>
<w>correlator</w>
<w>dinf</w>
<w>esnid</w>
<w>fleetsync</w>
<w>heterdyned</w>
<w>heterodynes</w>
<w>icecast</w>
<w>ident</w>
<w>nutall</w>
<w>overflowable</w>
<w>polyphase</w>
<w>qpsk</w>
<w>remez</w>
<w>resampler</w>
<w>samplerate</w>
<w>sdrtrunk</w>
<w>sheirer</w>
<w>shoutcast</w>
<w>streamable</w>
<w>talkgroup</w>
<w>tgid</w>
<w>ultravox</w>
<w>wacn</w>
<w>xtea</w>
</words>
</dictionary>
</component>

14
.idea/libraries/imports.xml generated Normal file
View File

@@ -0,0 +1,14 @@
<component name="libraryTable">
<library name="imports">
<CLASSES>
<root url="jar://$PROJECT_DIR$/imports/jmbe-interface-0.3.3.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/java-lame-3.98.4.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/radioreference-14.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/wsdl4j-1.5.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/commons-math3-3.6.1.jar!/" />
<root url="jar://$PROJECT_DIR$/imports/libresample4j-0.1.3.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</component>

2
.idea/misc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

10
.idea/vcs.xml generated
View File

@@ -1,5 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="IssueNavigationConfiguration">
<option name="links">
<list>
<IssueNavigationLink>
<option name="issueRegexp" value="#(\d+)" />
<option name="linkRegexp" value="https://github.com/DSheirer/sdrtrunk/issues/$1" />
</IssueNavigationLink>
</list>
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>

View File

@@ -1,4 +1,5 @@
0.3.4 20180210
0.3.4 20180418
- New P25 C4FM decoder that uses Differential QPSK decoder instead of FM demodulation with 4FSK.
- Adds channel auto-start and ordering capability with startup dialog allowing user to invoke or bypass channel
auto-start. Provides user with more control over startup configuration.
- Adds preferred tuner attribute to channel configuration allowing user to select a preferred tuner for the channel

View File

@@ -15,7 +15,7 @@ repositories {
mavenCentral()
}
version = '0.3.4-beta.1'
version = '0.4.0-alpha.4'
sourceCompatibility = 1.8
sourceSets {

View File

@@ -1,17 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014-2016 Dennis Sheirer
*
* This 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/>
******************************************************************************/

Binary file not shown.

Binary file not shown.

14
sdrtrunk.iml Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/images" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/config" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="imports" level="project" />
</component>
</module>

View File

@@ -24,6 +24,7 @@ import io.github.dsheirer.audio.output.StereoAudioOutput;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import io.github.dsheirer.source.mixer.MixerManager;
@@ -43,7 +44,7 @@ import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class AudioManager implements Listener<AudioPacket>, IAudioController
public class AudioManager implements Listener<ReusableAudioPacket>, IAudioController
{
private static final Logger mLog = LoggerFactory.getLogger(AudioManager.class);
@@ -58,7 +59,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
private LinkedTransferQueue<AudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private LinkedTransferQueue<ReusableAudioPacket> mAudioPacketQueue = new LinkedTransferQueue<>();
private Map<Integer,AudioOutputConnection> mChannelConnectionMap = new HashMap<>();
private List<AudioOutputConnection> mAudioOutputConnections = new ArrayList<>();
private AudioOutputConnection mLowestPriorityConnection;
@@ -170,7 +171,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
* Primary ingest point for audio produced by all decoding channels, for distribution to audio playback devices.
*/
@Override
public synchronized void receive(AudioPacket packet)
public synchronized void receive(ReusableAudioPacket packet)
{
mAudioPacketQueue.add(packet);
}
@@ -362,7 +363,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
* @param audioPacket from a decoding channel source
* @return an audio output connection or null
*/
private AudioOutputConnection getConnection(AudioPacket audioPacket)
private AudioOutputConnection getConnection(ReusableAudioPacket audioPacket)
{
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
@@ -410,6 +411,8 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
public class AudioPacketProcessor implements Runnable
{
private List<ReusableAudioPacket> mPackets = new ArrayList<>();
@Override
public void run()
{
@@ -419,14 +422,12 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
if(mAudioPacketQueue != null)
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mAudioPacketQueue.drainTo(mPackets);
mAudioPacketQueue.drainTo(packets);
for(AudioPacket packet : packets)
for(ReusableAudioPacket packet : mPackets)
{
/* Don't process any packet's marked as do not monitor */
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == AudioPacket.Type.AUDIO)
if(!packet.getMetadata().isDoNotMonitor() && packet.getType() == ReusableAudioPacket.Type.AUDIO)
{
AudioOutputConnection connection = getConnection(packet);
@@ -434,13 +435,29 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
{
connection.receive(packet);
}
else
{
packet.decrementUserCount();
}
}
else
{
packet.decrementUserCount();
}
}
mPackets.clear();
}
}
catch(Exception e)
{
mLog.error("Encountered error while processing audio packets", e);
while(mPackets.size() > 0)
{
ReusableAudioPacket audioPacket = mPackets.remove(0);
audioPacket.decrementUserCount();
}
}
}
}
@@ -463,7 +480,7 @@ public class AudioManager implements Listener<AudioPacket>, IAudioController
mAudioOutput = audioOutput;
}
public void receive(AudioPacket packet)
public void receive(ReusableAudioPacket packet)
{
if(packet.hasMetadata() && packet.getMetadata().getMetadataID() == mChannelMetadataID)
{

View File

@@ -1,19 +1,16 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is 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.audio;
@@ -23,92 +20,80 @@ import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.filter.polyphase.PolyphaseFIRDecimatingFilter_RB;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.IFilteredRealBufferListener;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.sample.buffer.IReusableBufferListener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledExecutorService;
/**
* Provides packaging of demodulated audio sample buffers into audio packets for
* broadcast to registered audio packet listeners. Includes audio packet
* metadata in constructed audio packets.
* Provides packaging of demodulated audio sample buffers into audio packets for broadcast to registered audio packet
* listeners. Includes audio packet metadata in constructed audio packets.
*
* Incorporates audio squelch state listener to control if audio packets are
* broadcast or ignored.
* Incorporates audio squelch state listener to control if audio packets are broadcast or ignored.
*
* This class is designed to support 8 kHz sample rate demodulated audio.
*/
public class AudioModule extends Module implements IAudioPacketProvider, IFilteredRealBufferListener,
ISquelchStateListener, Listener<RealBuffer>
public class AudioModule extends Module implements IAudioPacketProvider, IReusableBufferListener,
ISquelchStateListener, Listener<ReusableFloatBuffer>
{
protected static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("AudioModule");
private static float[] mDecimationCoefficients;
private static float[] sHighPassFilterCoefficients;
static
{
//Band-pass filter to both decimate (48kHz to 8kHz) and block 0 - 300 Hz (LTR signalling).
FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
.sampleRate(48000)
.stopFrequency1(150)
.passFrequencyBegin(300)
.passFrequencyEnd(3500)
.stopFrequency2(4000)
.stopRipple(0.0004)
.passRipple(0.008)
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
.sampleRate(8000)
.stopBandCutoff(200)
.stopBandAmplitude(0.0)
.stopBandRipple(0.025)
.passBandStart(300)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.build();
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
try
{
if(!designer.isValid())
{
throw new FilterDesignException("Couldn't design the audio decimation filter");
}
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
mDecimationCoefficients = designer.getImpulseResponse();
if(designer.isValid())
{
sHighPassFilterCoefficients = designer.getImpulseResponse();
}
}
catch(FilterDesignException e)
catch(FilterDesignException fde)
{
mLog.debug("Error designing filter", e);
mLog.error("Filter design error", fde);
}
}
private RealFIRFilter2 mHighPassFilter = new RealFIRFilter2(sHighPassFilterCoefficients);
private SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
private Listener<AudioPacket> mAudioPacketListener;
private PolyphaseFIRDecimatingFilter_RB mAudioDecimationFilter;
private Listener<ReusableAudioPacket> mAudioPacketListener;
private Metadata mMetadata;
/**
* Creates an Audio Module.
*
* @param metadata to use for audio packets produced by this audio module.
*/
public AudioModule(Metadata metadata)
{
mMetadata = metadata;
mAudioDecimationFilter = new PolyphaseFIRDecimatingFilter_RB(mDecimationCoefficients, 6, 2.0f);
mAudioDecimationFilter.setListener(new Listener<RealBuffer>()
{
@Override
public void receive(RealBuffer realBuffer)
{
if(mAudioPacketListener != null)
{
AudioPacket packet = new AudioPacket(realBuffer.getSamples(), mMetadata.copyOf());
mAudioPacketListener.receive(packet);
}
}
});
}
@Override
public void dispose()
{
mSquelchStateListener = null;
mAudioPacketListener = null;
mSquelchStateListener = null;
}
@Override
@@ -117,7 +102,7 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
/* No start operations provided */
}
@@ -125,34 +110,18 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
@Override
public void stop()
{
/* Issue an end audio packet in case a recorder is still rolling */
/* Issue an end-audio packet in case a recorder is still rolling */
if(mAudioPacketListener != null)
{
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
}
}
/**
* Processes demodulated audio samples into audio packets with current audio
* metadata and sends to the registered listener
*/
@Override
public void receive(RealBuffer buffer)
{
if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH)
{
mAudioDecimationFilter.receive(buffer);
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
endAudioPacket.setMetadata(mMetadata.copyOf());
endAudioPacket.incrementUserCount();
mAudioPacketListener.receive(endAudioPacket);
}
}
@Override
public Listener<RealBuffer> getFilteredRealBufferListener()
{
return this;
}
@Override
public void setAudioPacketListener(Listener<AudioPacket> listener)
public void setAudioPacketListener(Listener<ReusableAudioPacket> listener)
{
mAudioPacketListener = listener;
}
@@ -169,6 +138,34 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
return mSquelchStateListener;
}
@Override
public void receive(ReusableFloatBuffer reusableFloatBuffer)
{
if(mAudioPacketListener != null && mSquelchState == SquelchState.UNSQUELCH)
{
ReusableFloatBuffer highPassFiltered = mHighPassFilter.filter(reusableFloatBuffer);
ReusableAudioPacket audioPacket = mAudioPacketQueue.getBuffer(highPassFiltered.getSampleCount());
audioPacket.loadAudioFrom(highPassFiltered);
audioPacket.setMetadata(mMetadata.copyOf());
mAudioPacketListener.receive(audioPacket);
highPassFiltered.decrementUserCount();
}
else
{
reusableFloatBuffer.decrementUserCount();
}
}
@Override
public Listener getReusableBufferListener()
{
//Redirect received reusable buffers to the receive(buffer) method
return this;
}
/**
* Wrapper for squelch state listener
*/
@@ -179,7 +176,9 @@ public class AudioModule extends Module implements IAudioPacketProvider, IFilter
{
if(state == SquelchState.SQUELCH && mAudioPacketListener != null)
{
mAudioPacketListener.receive(new AudioPacket(AudioPacket.Type.END, mMetadata.copyOf()));
ReusableAudioPacket endAudioPacket = mAudioPacketQueue.getEndAudioBuffer();
endAudioPacket.setMetadata(mMetadata.copyOf());
mAudioPacketListener.receive(endAudioPacket);
}
mSquelchState = state;

View File

@@ -1,78 +0,0 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.audio;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.sample.real.RealBuffer;
public class AudioPacket
{
private Type mType;
private RealBuffer mAudioData;
private Metadata mMetadata;
public AudioPacket(Type type, Metadata metadata)
{
mType = type;
mMetadata = metadata;
}
public AudioPacket(float[] audio, Metadata metadata)
{
this(Type.AUDIO, metadata);
mAudioData = new RealBuffer(audio);
}
public boolean hasMetadata()
{
return mMetadata != null;
}
public Metadata getMetadata()
{
return mMetadata;
}
public void setMetadata(Metadata metadata)
{
mMetadata = metadata;
}
public Type getType()
{
return mType;
}
public RealBuffer getAudioBuffer()
{
return mAudioData;
}
public boolean hasAudioBuffer()
{
return mAudioData != null;
}
public enum Type
{
AUDIO,
END;
}
}

View File

@@ -1,55 +0,0 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AudioPacketRateCounter implements Listener<AudioPacket>
{
private final static Logger mLog =
LoggerFactory.getLogger( AudioPacketRateCounter.class );
private Listener<AudioPacket> mListener;
private long mCount;
private long mStartTime = -1;
private String mName;
public AudioPacketRateCounter()
{
mName = "UNNAMED";
}
public AudioPacketRateCounter( String name )
{
mName = name;
}
@Override
public void receive( AudioPacket packet )
{
if( mStartTime == -1 )
{
mStartTime = System.currentTimeMillis();
}
mCount += packet.getAudioBuffer().getSamples().length;
long elapsed = System.currentTimeMillis() - mStartTime;
if( elapsed != 0 )
{
mLog.debug( "[" + mName + "] Audio Rate: " + ( mCount / elapsed ) + " kHz" );
}
if( mListener != null )
{
mListener.receive( packet );
}
}
public void setListener( Listener<AudioPacket> listener )
{
mListener = listener;
}
}

View File

@@ -53,8 +53,8 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
private static ImageIcon MUTED_ICON = IconManager.getScaledIcon("/images/audio_muted.png", 20);
private static ImageIcon UNMUTED_ICON = IconManager.getScaledIcon("/images/audio_unmuted.png", 20);
private static ImageIcon MUTED_ICON = IconManager.getScaledIcon("images/audio_muted.png", 20);
private static ImageIcon UNMUTED_ICON = IconManager.getScaledIcon("images/audio_unmuted.png", 20);
private IconManager mIconManager;
private SettingsManager mSettingsManager;

View File

@@ -1,24 +1,22 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2016 Dennis Sheirer
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is 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.audio;
import io.github.dsheirer.sample.ConversionUtils;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,17 +32,18 @@ public class AudioUtils
/**
* Converts the audio packets into a byte array of 16-bit, little-endian audio samples
*/
public static byte[] convertTo16BitSamples(List<AudioPacket> audioPackets)
public static byte[] convertTo16BitSamples(List<ReusableAudioPacket> audioPackets)
{
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try
{
for(AudioPacket packet: audioPackets)
for(ReusableAudioPacket audioPacket: audioPackets)
{
//Converting from 32-bit floats to signed 16-bit samples
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(packet.getAudioBuffer().getSamples());
ByteBuffer buffer = ConversionUtils.convertToSigned16BitSamples(audioPacket.getAudioSamples());
stream.write(buffer.array());
audioPacket.decrementUserCount();
}
}
catch(IOException e)

View File

@@ -1,8 +1,9 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
public interface IAudioPacketListener
{
public Listener<AudioPacket> getAudioPacketListener();
public Listener<ReusableAudioPacket> getAudioPacketListener();
}

View File

@@ -1,9 +1,10 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
public interface IAudioPacketProvider
{
public void setAudioPacketListener( Listener<AudioPacket> listener );
public void setAudioPacketListener( Listener<ReusableAudioPacket> listener );
public void removeAudioPacketListener();
}

View File

@@ -245,7 +245,11 @@ public abstract class AudioBroadcaster implements Listener<AudioRecording>
{
if(mBroadcastState != state)
{
mLog.info("[" + getStreamName() + "] status: " + state);
if(state == BroadcastState.CONNECTED || state == BroadcastState.DISCONNECTED)
{
mLog.info("[" + getStreamName() + "] status: " + state);
}
mBroadcastState = state;
broadcast(new BroadcastEvent(this, BroadcastEvent.Event.BROADCASTER_STATE_CHANGE));

View File

@@ -19,12 +19,12 @@
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -46,7 +46,7 @@ import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BroadcastModel extends AbstractTableModel implements Listener<AudioPacket>
public class BroadcastModel extends AbstractTableModel implements Listener<ReusableAudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger(BroadcastModel.class);
@@ -276,7 +276,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
}
@Override
public void receive(AudioPacket audioPacket)
public void receive(ReusableAudioPacket audioPacket)
{
if(audioPacket.hasMetadata() && audioPacket.getMetadata().isStreamable())
{
@@ -284,11 +284,14 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
{
if(mBroadcasterMap.containsKey(channel.getChannelName()))
{
audioPacket.incrementUserCount();
mStreamManager.receive(audioPacket);
return;
}
}
}
audioPacket.decrementUserCount();
}
/**

View File

@@ -70,6 +70,11 @@ public enum BroadcastState
*/
MOUNT_POINT_IN_USE("Mount Point In Use", false),
/**
* Network is unavailable or the server address cannot be resolved
*/
NETWORK_UNAVAILABLE("Network Unavailable", false),
/**
* Server is not known or reachable
*/

View File

@@ -20,9 +20,14 @@ package io.github.dsheirer.audio.broadcast;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
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.*;
import java.awt.Color;
import java.awt.Component;
public class BroadcastStatusPanel extends JPanel
{
@@ -96,7 +101,8 @@ public class BroadcastStatusPanel extends JPanel
setBackground(table.getBackground());
setForeground(Color.LIGHT_GRAY);
}
else if(state == BroadcastState.INVALID_SETTINGS)
else if(state == BroadcastState.INVALID_SETTINGS ||
state == BroadcastState.NETWORK_UNAVAILABLE)
{
setBackground(Color.YELLOW);
setForeground(table.getForeground());

View File

@@ -18,9 +18,9 @@
******************************************************************************/
package io.github.dsheirer.audio.broadcast;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.record.AudioRecorder;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import org.slf4j.Logger;
@@ -37,7 +37,7 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
public class StreamManager implements Listener<AudioPacket>
public class StreamManager implements Listener<ReusableAudioPacket>
{
private final static Logger mLog = LoggerFactory.getLogger(StreamManager.class);
private static final long MAXIMUM_RECORDER_LIFESPAN_MILLIS = 30000; //30 seconds
@@ -115,7 +115,7 @@ public class StreamManager implements Listener<AudioPacket>
}
@Override
public void receive(AudioPacket audioPacket)
public void receive(ReusableAudioPacket audioPacket)
{
if(mRunning.get() && audioPacket.hasMetadata())
{
@@ -123,9 +123,9 @@ public class StreamManager implements Listener<AudioPacket>
{
int channelMetadataID = audioPacket.getMetadata().getMetadataID();
AudioPacket.Type type = audioPacket.getType();
ReusableAudioPacket.Type type = audioPacket.getType();
if(type == AudioPacket.Type.AUDIO)
if(type == ReusableAudioPacket.Type.AUDIO)
{
if(mStreamRecorders.containsKey(channelMetadataID))
{
@@ -133,6 +133,7 @@ public class StreamManager implements Listener<AudioPacket>
if(recorder != null)
{
audioPacket.incrementUserCount();
recorder.receive(audioPacket);
}
}
@@ -140,12 +141,13 @@ public class StreamManager implements Listener<AudioPacket>
{
AudioRecorder recorder = BroadcastFactory.getAudioRecorder(getTemporaryRecordingPath(),
mBroadcastFormat);
recorder.start(ThreadPool.SCHEDULED);
recorder.start();
audioPacket.incrementUserCount();
recorder.receive(audioPacket);
mStreamRecorders.put(channelMetadataID, recorder);
}
}
else if(type == AudioPacket.Type.END)
else if(type == ReusableAudioPacket.Type.END)
{
removeRecorder(channelMetadataID);
}
@@ -155,6 +157,8 @@ public class StreamManager implements Listener<AudioPacket>
}
}
}
audioPacket.decrementUserCount();
}
/**

View File

@@ -22,6 +22,7 @@ import io.github.dsheirer.audio.broadcast.IBroadcastMetadataUpdater;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
@@ -35,7 +36,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.nio.channels.UnresolvedAddressException;
@@ -117,70 +117,64 @@ public class IcecastBroadcastMetadataUpdater implements IBroadcastMetadataUpdate
{
mMetadataQueue.offer(getSong(metadata));
if(mUpdating.compareAndSet(false, true))
if(!mUpdating.get() && !mMetadataQueue.isEmpty())
{
String song = mMetadataQueue.poll();
while(song != null)
Runnable runnable = new Runnable()
{
HttpRequest updateRequest = createUpdateRequest(song);
if(updateRequest != null)
@Override
public void run()
{
ThreadPool.SCHEDULED.schedule(new Runnable()
if(!mMetadataQueue.isEmpty() && mUpdating.compareAndSet(false, true))
{
@Override
public void run()
try
{
try
{
ConnectFuture connectFuture = getSocketConnector()
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
mIcecastConfiguration.getPort()));
connectFuture.awaitUninterruptibly();
IoSession session = connectFuture.getSession();
ConnectFuture connectFuture = getSocketConnector()
.connect(new InetSocketAddress(mIcecastConfiguration.getHost(),
mIcecastConfiguration.getPort()));
connectFuture.awaitUninterruptibly();
IoSession session = connectFuture.getSession();
if(session != null)
{
session.write(updateRequest);
}
}
catch(UnresolvedAddressException uae)
if(session != null)
{
//Do nothing - the server is temporarily unavailable
}
catch(Exception e)
{
Throwable throwableCause = e.getCause();
String song = mMetadataQueue.poll();
if(throwableCause instanceof ConnectException)
while(song != null)
{
//Do nothing, the server is unavailable
}
else if(throwableCause instanceof UnresolvedAddressException)
{
//Do nothing - the server is temporarily unavailable
}
else
{
if(!mStackTraceLoggingSuppressed)
HttpRequest updateRequest = createUpdateRequest(song);
if(updateRequest != null)
{
mLog.error("Error sending metadata update. Future errors will " +
"be suppressed", e);
mStackTraceLoggingSuppressed = true;
session.write(updateRequest);
}
//Fetch next metadata update to send
song = mMetadataQueue.poll();
}
session.closeNow();
}
}
}, 0l, TimeUnit.SECONDS);
catch(UnresolvedAddressException | RuntimeIoException ure)
{
//Do nothing - the server is temporarily unavailable
}
catch(Exception e)
{
Throwable throwableCause = e.getCause();
if(!mStackTraceLoggingSuppressed)
{
mLog.error("Error sending metadata update. Future errors will be suppressed", e);
mStackTraceLoggingSuppressed = true;
}
}
mUpdating.set(false);
}
}
};
//Fetch next metadata update to send
song = mMetadataQueue.poll();
}
mUpdating.set(false);
ThreadPool.SCHEDULED.schedule(runnable, 0l, TimeUnit.SECONDS);
}
}

View File

@@ -27,15 +27,14 @@ import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.write.WriteTimeoutException;
import org.apache.mina.filter.codec.ProtocolCodecFilter;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.nio.channels.UnresolvedAddressException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -46,7 +45,8 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
private final static String TERMINATOR = "\r\n";
private final static String SEPARATOR = ":";
private static final long RECONNECT_INTERVAL_MILLISECONDS = 3000; //3 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT = 5000; //5 seconds
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; //5 seconds
private static final int WRITE_TIMEOUT_SECONDS = 5;
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
@@ -101,7 +101,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(CONNECTION_ATTEMPT_TIMEOUT);
mSocketConnector.getSessionConfig().setWriteTimeout(WRITE_TIMEOUT_SECONDS);
// LoggingFilter loggingFilter = new LoggingFilter(IcecastTCPAudioBroadcaster.class);
// loggingFilter.setMessageSentLogLevel(LogLevel.NONE);
@@ -127,45 +127,41 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT, TimeUnit.MILLISECONDS);
boolean connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
mStreamingSession = future.getSession();
if(connected)
{
mStreamingSession = future.getSession();
mConnecting.set(false);
return;
}
}
catch(RuntimeIoException rioe)
{
if(rioe.getCause() instanceof SocketException)
{
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
mConnecting.set(false);
return;
}
}
catch(UnresolvedAddressException uae)
{
//What do you do with this? Nothing.
setBroadcastState(BroadcastState.NETWORK_UNAVAILABLE);
mConnecting.set(false);
return;
}
catch(InterruptedException ie)
catch(Exception e)
{
//We couldn't connect within the timeout period -- disconnect so that we can try again later
disconnect();
}
catch(RuntimeIoException rie)
{
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("[" + getStreamName() + "] failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.ERROR);
mLog.debug("[" + getStreamName() + "] failed to connect - no exception is available");
}
disconnect();
mLog.error("Error", e);
//Disregard ... we'll disconnect and try again
}
catch(Throwable t)
{
disconnect();
mLog.error("Throwable error caught", t);
}
disconnect();
mConnecting.set(false);
}
};
@@ -188,6 +184,7 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
}
else
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLastConnectionAttempt = System.currentTimeMillis();
}
}
@@ -263,49 +260,12 @@ public class IcecastTCPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception
{
if(cause instanceof IOException)
{
IOException ioe = (IOException)cause;
if(ioe.getMessage() != null)
{
String reason = ioe.getMessage();
if(connected())
{
disconnect();
}
else if(reason.startsWith("Connection reset"))
{
disconnect();
}
else if(reason.startsWith("Operation timed out"))
{
disconnect();
}
else
{
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
}
else if(ioe instanceof WriteTimeoutException)
{
disconnect();
}
else
{
setBroadcastState(BroadcastState.ERROR);
disconnect();
mLog.error("[" + getStreamName() + "] Unspecified IO error - streaming halted.", cause);
}
}
else
if(!(cause instanceof IOException))
{
mLog.error("[" + getStreamName() + "] Broadcast error", cause);
setBroadcastState(BroadcastState.ERROR);
disconnect();
}
disconnect();
}
@Override

View File

@@ -18,7 +18,7 @@
******************************************************************************/
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import java.util.List;
@@ -27,7 +27,7 @@ public interface IAudioConverter
/**
* Converts the PCM audio packets to converted audio format. May produce partial audio frame data.
*/
public byte[] convert(List<AudioPacket> audioPackets);
public byte[] convert(List<ReusableAudioPacket> audioPackets);
/**
* Finalizes audio conversion by fully converting any partial frames left in the buffer and returning the

View File

@@ -19,8 +19,8 @@
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.audio.AudioUtils;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import net.sourceforge.lame.lowlevel.LameEncoder;
import net.sourceforge.lame.mp3.Lame;
import net.sourceforge.lame.mp3.MPEGMode;
@@ -54,7 +54,7 @@ public class MP3AudioConverter implements IAudioConverter
}
@Override
public byte[] convert(List<AudioPacket> audioPackets)
public byte[] convert(List<ReusableAudioPacket> audioPackets)
{
mMP3Stream.reset();

View File

@@ -18,8 +18,9 @@
******************************************************************************/
package io.github.dsheirer.audio.convert;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.record.mp3.MP3Recorder;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacketQueue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -33,6 +34,7 @@ public class MP3SilenceGenerator implements ISilenceGenerator
private MP3AudioConverter mGenerator = new MP3AudioConverter(MP3Recorder.MP3_BIT_RATE, MP3Recorder.CONSTANT_BIT_RATE);
private byte[] mPreviousPartialFrameData;
private ReusableAudioPacketQueue mAudioPacketQueue = new ReusableAudioPacketQueue("MP3 Silence Generator");
/**
* Generates MP3 audio silence frames
@@ -44,9 +46,10 @@ public class MP3SilenceGenerator implements ISilenceGenerator
public byte[] generate(long duration)
{
int length = (int)(duration * 8); //8000 Hz sample rate
float[] silence = new float[length];
AudioPacket silencePacket = new AudioPacket(silence, null);
List<AudioPacket> silencePackets = new ArrayList<>();
ReusableAudioPacket silencePacket = mAudioPacketQueue.getBuffer(length);
Arrays.fill(silencePacket.getAudioSamples(), 0.0f);
List<ReusableAudioPacket> silencePackets = new ArrayList<>();
silencePackets.add(silencePacket);
byte[] frameData = mGenerator.convert(silencePackets);

View File

@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
/**
@@ -29,7 +30,7 @@ import io.github.dsheirer.dsp.mixer.Oscillator;
public class AudioAdapter
{
private static int mGain = 5;
private Oscillator mSineWaveGenerator;
private IOscillator mSineWaveGenerator;
private FloatFIRFilter mPostInversionFilter;
private FloatFIRFilter mAudioHighPassFilter;
private AudioType mAudioType = AudioType.NORMAL;
@@ -88,7 +89,7 @@ public class AudioAdapter
default:
retVal = (float)( mPostInversionFilter.get(
mAudioHighPassFilter.get( sample ) *
mSineWaveGenerator.getFloat() ) );
mSineWaveGenerator.inphase() ) );
mSineWaveGenerator.rotate();
break;

View File

@@ -20,6 +20,7 @@ package io.github.dsheirer.audio.invert;
import io.github.dsheirer.audio.InversionFrequency;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.FloatFIRFilter;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.real.RealSampleListener;
@@ -29,7 +30,7 @@ import io.github.dsheirer.sample.real.RealSampleListener;
*/
public class AudioInverter implements RealSampleListener
{
private Oscillator mSineWaveGenerator;
private IOscillator mSineWaveGenerator;
private FloatFIRFilter mPostInversionLowPassFilter;
private RealSampleListener mListener;
@@ -61,7 +62,7 @@ public class AudioInverter implements RealSampleListener
if( mPostInversionLowPassFilter != null )
{
mPostInversionLowPassFilter.receive(
sample * mSineWaveGenerator.getFloat() );
sample * mSineWaveGenerator.inphase() );
mSineWaveGenerator.rotate();
}

View File

@@ -19,10 +19,10 @@
package io.github.dsheirer.audio.output;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.channel.metadata.Metadata;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
@@ -46,11 +46,11 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
public abstract class AudioOutput implements Listener<ReusableAudioPacket>, LineListener
{
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
private LinkedTransferQueue<AudioPacket> mBuffer = new LinkedTransferQueue<>();
private LinkedTransferQueue<ReusableAudioPacket> mBuffer = new LinkedTransferQueue<>();
private int mBufferStartThreshold;
private int mBufferStopThreshold;
@@ -190,7 +190,7 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
* Converts the audio packet data into a byte buffer format appropriate for
* the underlying source data line.
*/
protected abstract ByteBuffer convert(AudioPacket packet);
protected abstract ByteBuffer convert(ReusableAudioPacket packet);
/**
* Audio output channel name
@@ -271,7 +271,7 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
}
@Override
public void receive(AudioPacket packet)
public void receive(ReusableAudioPacket packet)
{
if(mCanProcessAudio)
{
@@ -281,11 +281,17 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
mBuffer.add(packet);
}
else
{
packet.decrementUserCount();
}
}
public class BufferProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private List<ReusableAudioPacket> mAudioPackets = new ArrayList<ReusableAudioPacket>();
public BufferProcessor()
{
@@ -300,13 +306,11 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
* processor can run at any given time */
if(mProcessing.compareAndSet(false, true))
{
List<AudioPacket> packets = new ArrayList<AudioPacket>();
mBuffer.drainTo(mAudioPackets);
mBuffer.drainTo(packets);
for(AudioPacket packet : packets)
for(ReusableAudioPacket packet : mAudioPackets)
{
if(packet.getType() == AudioPacket.Type.AUDIO)
if(packet.getType() == ReusableAudioPacket.Type.AUDIO)
{
broadcast(packet.getMetadata());
@@ -338,8 +342,14 @@ public abstract class AudioOutput implements Listener<AudioPacket>, LineListener
updateTimestamp();
}
else
{
packet.decrementUserCount();
}
}
mAudioPackets.clear();
checkStop();
mProcessing.set(false);

View File

@@ -19,7 +19,7 @@
package io.github.dsheirer.audio.output;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
@@ -43,14 +43,16 @@ public class MonoAudioOutput extends AudioOutput
/**
* Converts the audio packet data into mono audio frames.
*/
protected ByteBuffer convert(AudioPacket packet)
protected ByteBuffer convert(ReusableAudioPacket packet)
{
if(packet.hasAudioBuffer())
ByteBuffer buffer = null;
if(packet.hasAudioSamples())
{
float[] samples = packet.getAudioBuffer().getSamples();
float[] samples = packet.getAudioSamples();
/* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
@@ -58,10 +60,10 @@ public class MonoAudioOutput extends AudioOutput
{
shortBuffer.put((short) (sample * Short.MAX_VALUE));
}
return buffer;
}
return null;
packet.decrementUserCount();
return buffer;
}
}

View File

@@ -19,7 +19,7 @@
package io.github.dsheirer.audio.output;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
@@ -45,15 +45,16 @@ public class StereoAudioOutput extends AudioOutput
* channel containing the audio and the other channel containing zero
* valued (silent) samples.
*/
protected ByteBuffer convert(AudioPacket packet)
protected ByteBuffer convert(ReusableAudioPacket packet)
{
if(packet.hasAudioBuffer())
ByteBuffer buffer = null;
if(packet.hasAudioSamples())
{
float[] samples = packet.getAudioBuffer().getSamples();
float[] samples = packet.getAudioSamples();
/* Little-endian byte buffer */
ByteBuffer buffer = ByteBuffer.allocate(samples.length * 4)
.order(ByteOrder.LITTLE_ENDIAN);
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
@@ -73,10 +74,10 @@ public class StereoAudioOutput extends AudioOutput
shortBuffer.put((short) (sample * Short.MAX_VALUE));
}
}
return buffer;
}
return null;
packet.decrementUserCount();
return buffer;
}
}

View File

@@ -1,40 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This 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.squelch;
/**
* Provides squelch control based on presense of decoded audio samples.
*
* NOTE: this class is not yet fully implemented with audio sample processing
* and currently only provides user driven squelch control
*/
public class ManualSquelchController extends SquelchController
{
public ManualSquelchController()
{
mSquelchMode = SquelchMode.NONE;
}
@Override
public void setSquelchMode( SquelchMode mode )
{
//TODO: add functionality
}
}

View File

@@ -1,122 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This 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.squelch;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.sample.real.RealSampleListener;
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
/**
* Squelch control - receives single audio samples or real audio buffers and
* adjusts squelch state either based on the audio samples or based on external
* control inputs.
*
* Assembles received single audio samples into real sample buffers for
* consumption by an external audio processor, like the AudioPacketProcessor
* class.
*/
public abstract class SquelchController
implements Listener<RealBuffer>, RealSampleListener
{
private Broadcaster<SquelchState> mSquelchStateBroadcaster = new Broadcaster<>();
private Listener<RealBuffer> mAudioListener;
protected SquelchState mSquelchState = SquelchState.UNSQUELCH;
protected SquelchMode mSquelchMode = SquelchMode.MANUAL;
private RealToRealBufferAssembler mSampleAssembler = new RealToRealBufferAssembler( 512 );
public SquelchController()
{
mSampleAssembler.setListener( this );
}
@Override
public void receive( float sample )
{
mSampleAssembler.receive( sample );
}
@Override
public void receive( RealBuffer buffer )
{
if( mAudioListener != null )
{
mAudioListener.receive( buffer );
}
}
public abstract void setSquelchMode( SquelchMode mode );
public SquelchMode getSquelchMode()
{
return mSquelchMode;
}
public void setSquelchState( SquelchState state )
{
mSquelchState = state;
}
public SquelchState getSquelchState()
{
return mSquelchState;
}
public boolean isSquelched()
{
switch( mSquelchMode )
{
case AUTOMATIC:
case MANUAL:
return mSquelchState == SquelchState.SQUELCH;
case NONE:
return false;
default:
return false;
}
}
public void addSquelchStateListener( Listener<SquelchState> listener )
{
if( listener != null )
{
mSquelchStateBroadcaster.addListener( listener );
/* Send current squelch state to the listener */
listener.receive( mSquelchState );
}
}
public void removeSquelchStateListener( Listener<SquelchState> listener )
{
mSquelchStateBroadcaster.removeListener( listener );
}
public void setAudioListener( Listener<RealBuffer> listener )
{
mAudioListener = listener;
}
public void removeAudioListener( Listener<RealBuffer> listener )
{
mAudioListener = null;
}
}

View File

@@ -1,55 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
* This 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.squelch;
public class TrunkingSquelchController extends SquelchController
{
private boolean mAllowSquelchOverride = true;
public TrunkingSquelchController()
{
}
/**
* Provides control over audio stream as directed by an external trunking
* channel state.
*
* Override automatic squelch control by setting the squelch mode to none.
*
* Disable automatic control override by setting allowManualOverride to false;
*
* Note: this control does not respond to squelch mode manual.
*/
public TrunkingSquelchController( boolean allowSquelchOverride )
{
mAllowSquelchOverride = allowSquelchOverride;
mSquelchMode = SquelchMode.AUTOMATIC;
}
@Override
public void setSquelchMode( SquelchMode mode )
{
if( mode == SquelchMode.AUTOMATIC ||
( mode == SquelchMode.NONE && mAllowSquelchOverride ) )
{
mSquelchMode = mode;
}
}
}

View File

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

View File

@@ -1,6 +0,0 @@
package io.github.dsheirer.bits;
public interface ISyncDetectListener
{
public void syncDetected();
}

View File

@@ -1,6 +1,26 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.bits;
public interface ISyncProcessor
{
public void checkSync( long value );
/**
* Checks if the value matches a known sync pattern
* @param value
* @return
*/
boolean checkSync(long value);
}

View File

@@ -1,119 +1,116 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.SyncDetectListener;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import io.github.dsheirer.dsp.symbol.SyncDetectProvider;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* MessageFramer - processes bitsets looking for a sync pattern within
* the bits, and then extracts the message, including the sync
* pattern, for a total bit length of messageLength.
*
*
* Will extract multiple messages simultaneously, for each sync pattern that is
* encountered within the bitset bit stream.
*/
public class MessageFramer implements Listener<Boolean>,
SyncDetectProvider
public class MessageFramer implements IBinarySymbolProcessor, Listener<Boolean>, SyncDetectProvider
{
private boolean[] mSyncPattern;
private int mMessageLength;
private SyncDetectListener mSyncDetectListener;
private Broadcaster<BinaryMessage> mBroadcaster =
new Broadcaster<BinaryMessage>();
private ArrayList<MessageAssembler> mMessageAssemblers =
new ArrayList<MessageAssembler>();
private boolean[] mSyncPattern;
private int mMessageLength;
private ISyncDetectListener mSyncDetectListener;
private Broadcaster<BinaryMessage> mBroadcaster = new Broadcaster<BinaryMessage>();
private List<MessageAssembler> mMessageAssemblers = new ArrayList<MessageAssembler>();
private List<MessageAssembler> mCompletedMessageAssemblers = new ArrayList<MessageAssembler>();
private SyncPatternMatcher mMatcher;
private ArrayList<MessageAssembler> mCompletedMessageAssemblers =
new ArrayList<MessageAssembler>();
private BinaryMessage mPreviousBuffer = null;
private SyncPatternMatcher mMatcher;
public MessageFramer( boolean[] syncPattern, int messageLength )
public MessageFramer(boolean[] syncPattern, int messageLength)
{
mSyncPattern = syncPattern;
mMatcher = new SyncPatternMatcher( syncPattern );
mMatcher = new SyncPatternMatcher(syncPattern);
mMessageLength = messageLength;
}
public void reset()
{
for( MessageAssembler assembler: mMessageAssemblers )
{
assembler.dispose();
}
for(MessageAssembler assembler : mMessageAssemblers)
{
assembler.dispose();
}
mMessageAssemblers.clear();
}
public void dispose()
{
mBroadcaster.dispose();
mCompletedMessageAssemblers.clear();
mMessageAssemblers.clear();
}
@Override
public void receive( Boolean bit )
public void dispose()
{
mMatcher.receive( bit );
mBroadcaster.dispose();
mCompletedMessageAssemblers.clear();
mMessageAssemblers.clear();
}
public void receive(boolean bit)
{
mMatcher.receive(bit);
Iterator<MessageAssembler> it = mMessageAssemblers.iterator();
MessageAssembler assembler;
while( it.hasNext() )
while(it.hasNext())
{
assembler = it.next();
/* Dispose and remove any completed assemblers */
if( assembler.complete() )
if(assembler.complete())
{
assembler.dispose();
it.remove();
assembler.dispose();
it.remove();
}
/* Otherwise, send them the bit */
else
{
assembler.receive( bit );
assembler.receive(bit);
}
}
/* Check for sync match and add new message assembler */
if( mMatcher.matches() )
{
addMessageAssembler( new MessageAssembler( mMessageLength, mSyncPattern ) );
if(mMatcher.matches())
{
addMessageAssembler(new MessageAssembler(mMessageLength, mSyncPattern));
/* Notify any sync detect listener(s) */
if( mSyncDetectListener != null )
if(mSyncDetectListener != null)
{
mSyncDetectListener.syncDetected();
mSyncDetectListener.syncDetected();
}
}
}
}
@Deprecated //Legacy support ... remove once all producers are converted to use receive(boolean bit) method
@Override
public void receive(Boolean bit)
{
receive(bit);
}
/**
* Causes all messages currently under assembly to be forcibly
* sent (ie flushed) to all registered message listeners, and
@@ -121,44 +118,44 @@ public class MessageFramer implements Listener<Boolean>,
*/
public void flush()
{
for( MessageAssembler assembler: mMessageAssemblers )
for(MessageAssembler assembler : mMessageAssemblers)
{
assembler.flush();
}
}
@Override
public void setSyncDetectListener( SyncDetectListener listener )
@Override
public void setSyncDetectListener(ISyncDetectListener listener)
{
mSyncDetectListener = listener;
mSyncDetectListener = listener;
}
/**
* Allow a message listener to register with this framer to receive
* all framed messages
*/
public void addMessageListener( Listener<BinaryMessage> listener )
public void addMessageListener(Listener<BinaryMessage> listener)
{
mBroadcaster.addListener( listener );
mBroadcaster.addListener(listener);
}
public void removeMessageListener( Listener<BinaryMessage> listener )
public void removeMessageListener(Listener<BinaryMessage> listener)
{
mBroadcaster.removeListener( listener );
mBroadcaster.removeListener(listener);
}
/*
* Adds a message assembler to receive bits from the bit stream
*/
private void addMessageAssembler( MessageAssembler assembler )
private void addMessageAssembler(MessageAssembler assembler)
{
mMessageAssemblers.add( assembler );
mMessageAssemblers.add(assembler);
}
@SuppressWarnings( "unused" )
private void removeMessageAssembler( MessageAssembler assembler )
@SuppressWarnings("unused")
private void removeMessageAssembler(MessageAssembler assembler)
{
mMessageAssemblers.remove( assembler );
mMessageAssemblers.remove(assembler);
}
/**
@@ -166,7 +163,7 @@ public class MessageFramer implements Listener<Boolean>,
* identified sync pattern, and every bit thereafter. Once the accumulated
* bits equal the message length, the message is sent and the assembler
* flags itself as complete.
*
*
* By design, multiple message assemblers can exist at the same time, each
* assembling different, overlapping potential messages
*/
@@ -174,46 +171,46 @@ public class MessageFramer implements Listener<Boolean>,
{
BinaryMessage mMessage;
boolean mComplete = false;
MessageAssembler( int messageLength )
MessageAssembler(int messageLength)
{
mMessage = new BinaryMessage( messageLength );
mMessage = new BinaryMessage(messageLength);
}
MessageAssembler( int messageLength, boolean[] initialFill )
MessageAssembler(int messageLength, boolean[] initialFill)
{
this( messageLength );
this(messageLength);
/* Pre-load the message with the sync pattern */
for( int x = 0; x < initialFill.length; x++ )
for(int x = 0; x < initialFill.length; x++)
{
receive( initialFill[ x ] );
receive(initialFill[x]);
}
}
public void dispose()
{
mMessage = null;
mMessage = null;
}
@Override
/**
* Receives one bit at a time, and assembles them into a message
*/
public void receive( Boolean bit )
public void receive(Boolean bit)
{
try
{
mMessage.add( bit );
mMessage.add(bit);
}
catch( BitSetFullException e )
catch(BitSetFullException e)
{
e.printStackTrace();
}
/* Once our message is complete (ie full), send it to all registered
* message listeners, and set complete flag so for auto-removal */
if( mMessage.isFull() )
if(mMessage.isFull())
{
mComplete = true;
flush();
@@ -226,7 +223,7 @@ public class MessageFramer implements Listener<Boolean>,
*/
public void flush()
{
mBroadcaster.receive( mMessage );
mBroadcaster.receive(mMessage);
mComplete = true;
}

View File

@@ -1,111 +1,130 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* Multi-Sync pattern matcher. Accepts multiple sync detector objects for
* Multi-Sync pattern matcher. Accepts multiple sync detector objects for
* comparision against the incoming bit stream.
*
*
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
* If a 64 bit or larger sync pattern is applied, then the wrapping feature
* of the Long.rotate methods will wrap the MSB around to the LSB and
* corrupt the matching value.
*
*/
public class MultiSyncPatternMatcher
{
private List<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
private long mBits = 0;
private long mMask = 0;
public MultiSyncPatternMatcher( int syncSize )
{
Validate.isTrue(syncSize < 64);
//Setup a bit mask of all ones the length of the sync pattern
mMask = (long)( ( Math.pow( 2, syncSize ) ) - 1 );
}
public void dispose()
{
mSyncProcessors.clear();
}
/**
* Processes two bits before checking sync processors for a match.
*/
public void receive( boolean bit1, boolean bit2 )
{
mBits = Long.rotateLeft( mBits, 1 );
mBits &= mMask;
if( bit1 )
{
mBits += 1;
}
private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class);
mBits = Long.rotateLeft( mBits, 1 );
mBits &= mMask;
if( bit2 )
{
mBits += 1;
}
private ISyncDetectListener mSyncDetectListener;
private int mSyncLossThreshold;
private int mBitCount;
private List<ISyncProcessor> mSyncProcessors = new ArrayList<ISyncProcessor>();
for( ISyncProcessor processor: mSyncProcessors )
{
processor.checkSync( mBits );
}
}
/**
* Processes one bit before checking sync processors for a match.
*/
public void receive( boolean bit )
{
mBits = Long.rotateLeft( mBits, 1 );
mBits &= mMask;
if( bit )
{
mBits += 1;
}
private long mBits = 0;
private long mMask = 0;
for( ISyncProcessor processor: mSyncProcessors )
{
processor.checkSync( mBits );
}
}
public MultiSyncPatternMatcher(ISyncDetectListener syncDetectListener, int syncLossThreshold, int syncSize)
{
mSyncDetectListener = syncDetectListener;
mSyncLossThreshold = syncLossThreshold;
Validate.isTrue(syncSize < 64);
/**
* Adds a sync processor to receive the bit stream.
*/
public void add( ISyncProcessor processor )
{
mSyncProcessors.add( processor );
}
//Setup a bit mask of all ones the length of the sync pattern
mMask = (long)((Math.pow(2, syncSize)) - 1);
}
public void dispose()
{
mSyncProcessors.clear();
}
/**
* Processes two bits before checking sync processors for a match.
*/
public void receive(boolean bit1, boolean bit2)
{
mBits = Long.rotateLeft(mBits, 1);
mBits &= mMask;
if(bit1)
{
mBits += 1;
}
mBits = Long.rotateLeft(mBits, 1);
mBits &= mMask;
if(bit2)
{
mBits += 1;
}
mBitCount += 2;
for(ISyncProcessor processor : mSyncProcessors)
{
if(processor.checkSync(mBits))
{
mSyncDetectListener.syncDetected();
mBitCount = 0;
}
}
if(mBitCount > mSyncLossThreshold)
{
mBitCount = 0;
mSyncDetectListener.syncLost();
}
}
/**
* Processes one bit before checking sync processors for a match.
*/
public void receive(boolean bit)
{
mBits = Long.rotateLeft(mBits, 1);
mBits &= mMask;
if(bit)
{
mBits += 1;
}
for(ISyncProcessor processor : mSyncProcessors)
{
processor.checkSync(mBits);
}
}
/**
* Adds a sync processor to receive the bit stream.
*/
public void add(ISyncProcessor processor)
{
mSyncProcessors.add(processor);
}
}

View File

@@ -1,48 +1,68 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
public class SoftSyncDetector implements ISyncProcessor
{
private ISyncDetectListener mListener;
private long mPattern;
private int mThreshold;
public SoftSyncDetector( ISyncDetectListener listener, long pattern, int threshold )
{
this( pattern, threshold );
mListener = listener;
}
public SoftSyncDetector( long pattern, int threshold )
{
mPattern = pattern;
mThreshold = threshold;
}
public void dispose()
{
mListener = null;
}
private ISyncDetectListener mListener;
private long mPattern;
private int mThreshold;
@Override
public void checkSync( long value )
{
long difference = value ^ mPattern;
if( ( difference == 0 || Long.bitCount( difference ) <= mThreshold ) &&
mListener != null )
{
mListener.syncDetected();
}
}
public void setThreshold( int threshold )
{
mThreshold = threshold;
}
public void setListener( ISyncDetectListener listener )
{
mListener = listener;
}
public SoftSyncDetector(ISyncDetectListener listener, long pattern, int threshold)
{
this(pattern, threshold);
mListener = listener;
}
public SoftSyncDetector(long pattern, int threshold)
{
mPattern = pattern;
mThreshold = threshold;
}
public void dispose()
{
mListener = null;
}
@Override
public boolean checkSync(long value)
{
long difference = value ^ mPattern;
if((difference == 0 || Long.bitCount(difference) <= mThreshold) &&
mListener != null)
{
mListener.syncDetected();
return true;
}
return false;
}
public void setThreshold(int threshold)
{
mThreshold = threshold;
}
public void setListener(ISyncDetectListener listener)
{
mListener = listener;
}
}

View File

@@ -1,33 +1,57 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
public class SyncDetector implements ISyncProcessor
{
private long mPattern;
private ISyncDetectListener mListener;
private long mPattern;
private ISyncDetectListener mListener;
public SyncDetector( long pattern )
{
mPattern = pattern;
}
public SyncDetector( long pattern, ISyncDetectListener listener )
{
this( pattern );
mListener = listener;
}
public void setListener( ISyncDetectListener listener )
{
mListener = listener;
}
public SyncDetector(long pattern)
{
mPattern = pattern;
}
@Override
public void checkSync( long value )
{
if( value == mPattern && mListener != null )
{
mListener.syncDetected();
}
}
public SyncDetector(long pattern, ISyncDetectListener listener)
{
this(pattern);
mListener = listener;
}
public void setListener(ISyncDetectListener listener)
{
mListener = listener;
}
@Override
public boolean checkSync(long value)
{
if(value == mPattern)
{
if(mListener != null)
{
mListener.syncDetected();
}
return true;
}
return false;
}
}

View File

@@ -29,7 +29,7 @@ public enum SyncPattern
false, false, false, false //0000 0x0
} ),
/* Revs (0x0A) and Sync (0x23EB) = 10101 0010001111101011 */
/* Revs (0x0A) and Sync (0x23EB) = 0101 000 1 000 11111 0 1 0 11 */
FLEETSYNC2( new boolean[]
{
false, true, false, true, false, //End of bit revs

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
@@ -20,113 +20,112 @@ package io.github.dsheirer.bits;
/**
* Sync pattern matcher.
*
*
* Note: works for sync patterns up to 63 bits (integer size - 1 ) long.
* If a 64 bit or larger sync pattern is applied, then the wrapping feature
* of the Long.rotate methods will wrap the MSB around to the LSB and
* corrupt the matching value.
*
*/
public class SyncPatternMatcher
{
private long mBits = 0;
private long mMask = 0;
private long mSync = 0;
private boolean mSoftMode = false;
private int mSoftModeErrorThreshold = 0;
public SyncPatternMatcher( boolean[] syncPattern )
{
//Setup a bit mask of all ones the length of the sync pattern
mMask = (long)( ( Math.pow( 2, syncPattern.length ) ) - 1 );
//Convert the sync bits into a long value for comparison
for( int x = 0; x < syncPattern.length; x++ )
{
if( syncPattern[ x ] )
{
mSync += 1l << ( syncPattern.length - 1 - x );
}
}
}
public SyncPatternMatcher( long sync )
{
mSync = sync;
mMask = getMask( sync );
}
private long mBits = 0;
private long mMask = 0;
private long mSync = 0;
private boolean mSoftMode = false;
private int mSoftModeErrorThreshold = 0;
/**
* Constructs the sync pattern matcher and defines the soft mode maximum
* number of bit errors to allow in the pattern match.
*
* @param sync pattern
* @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
* positions) when considering a sync match
*/
public SyncPatternMatcher( long sync, int softModeErrorThreshold )
{
this( sync );
mSoftModeErrorThreshold = softModeErrorThreshold;
}
public SyncPatternMatcher(boolean[] syncPattern)
{
//Setup a bit mask of all ones the length of the sync pattern
mMask = (long)((Math.pow(2, syncPattern.length)) - 1);
/**
* Generates an all ones mask corresponding to the sync pattern
*/
private static long getMask( long sync )
{
long mask = sync;
for( int x = 1; x < 64; x *= 2 )
{
mask |= ( mask >> x );
}
return mask;
}
public void receive( boolean bit )
{
//Left shift the previous value
mBits = Long.rotateLeft( mBits, 1 );
//Apply the mask to erase the previous MSB
mBits &= mMask;
//Add in the new bit
if( bit )
{
mBits += 1;
}
}
//Convert the sync bits into a long value for comparison
for(int x = 0; x < syncPattern.length; x++)
{
if(syncPattern[x])
{
mSync += 1l << (syncPattern.length - 1 - x);
}
}
}
/**
* Indicates if the most recently received bit sequence matches the
* sync pattern
*/
public boolean matches()
{
if( mSoftMode )
{
long difference = mBits ^ mSync;
return difference == 0 ||
Long.bitCount( difference ) <= mSoftModeErrorThreshold;
}
else
{
return ( mBits == mSync );
}
}
public SyncPatternMatcher(long sync)
{
mSync = sync;
mMask = getMask(sync);
}
/**
* Sets the sync pattern matcher in soft mode which allows up to the soft
* mode threshold number of bit errors when considering a sync pattern match
*/
public void setSoftMode( boolean softMode )
{
mSoftMode = softMode;
}
/**
* Constructs the sync pattern matcher and defines the soft mode maximum
* number of bit errors to allow in the pattern match.
*
* @param sync pattern
* @param softModeErrorThreshold - maximum bit errors (ie pattern mismatch
* positions) when considering a sync match
*/
public SyncPatternMatcher(long sync, int softModeErrorThreshold)
{
this(sync);
mSoftModeErrorThreshold = softModeErrorThreshold;
}
/**
* Generates an all ones mask corresponding to the sync pattern
*/
private static long getMask(long sync)
{
long mask = sync;
for(int x = 1; x < 64; x *= 2)
{
mask |= (mask >> x);
}
return mask;
}
public void receive(boolean bit)
{
//Left shift the previous value
mBits = Long.rotateLeft(mBits, 1);
//Apply the mask to erase the previous MSB
mBits &= mMask;
//Add in the new bit
if(bit)
{
mBits += 1;
}
}
/**
* Indicates if the most recently received bit sequence matches the
* sync pattern
*/
public boolean matches()
{
if(mSoftMode)
{
long difference = mBits ^ mSync;
return difference == 0 ||
Long.bitCount(difference) <= mSoftModeErrorThreshold;
}
else
{
return (mBits == mSync);
}
}
/**
* Sets the sync pattern matcher in soft mode which allows up to the soft
* mode threshold number of bit errors when considering a sync pattern match
*/
public void setSoftMode(boolean softMode)
{
mSoftMode = softMode;
}
}

View File

@@ -1,107 +1,89 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.buffer;
import java.util.Arrays;
/**
* Circular Buffer - implements a circular buffer with a method to get the
* current head of the buffer, and put a new value in its place at the same
* time
*
*
* Also provides averaging over the elements in the buffer
*/
public class BooleanAveragingBuffer {
public class BooleanAveragingBuffer
{
private boolean[] mBuffer;
private int mBufferPointer;
private int mThreshold;
private int mTrueCount;
private boolean[] mBuffer;
private int mBufferPointer;
public BooleanAveragingBuffer( int length )
{
mBuffer = new boolean[ length ];
//Preload the array with false values
Arrays.fill( mBuffer, false );
}
public boolean get( boolean newValue )
{
//Fetch current boolean value
boolean retVal = mBuffer[ mBufferPointer ];
public BooleanAveragingBuffer(int length)
{
mBuffer = new boolean[length];
mThreshold = length / 2;
}
//Load the new value into the buffer
put( newValue );
return retVal;
}
public boolean get(boolean newValue)
{
//Fetch current boolean value
boolean retVal = mBuffer[mBufferPointer];
/**
* Loads the newValue into this buffer and adjusts the buffer pointer
* to prepare for the next get/put cycle
*/
private void put( boolean newValue )
{
//Store the new value to the buffer
mBuffer[ mBufferPointer ] = newValue;
//Increment the buffer pointer
mBufferPointer++;
//Load the new value into the buffer
put(newValue);
//Wrap the buffer pointer around to 0 when necessary
if( mBufferPointer >= mBuffer.length )
{
mBufferPointer = 0;
}
}
return retVal;
}
/**
* Loads the newValue into the buffer, calculates the average
* and returns that average from this method
*
* This effectively performs low-pass filtering
*/
public boolean getAverage( boolean newValue )
{
//Load the new value into the buffer
put( newValue );
int trueCount = 0;
for( int x = 0; x < mBuffer.length; x++ )
{
if( mBuffer[ x ] )
{
trueCount++;
}
}
/**
* Loads the newValue into this buffer and adjusts the buffer pointer
* to prepare for the next get/put cycle
*/
private void put(boolean newValue)
{
//Store the new value to the buffer
mBuffer[mBufferPointer] = newValue;
/**
* If the number of true values in the buffer is
* more than half, return true, otherwise, false
*/
if( trueCount > (int)( mBuffer.length / 2 ) )
{
return true;
}
else
{
return false;
}
}
//Increment the buffer pointer
mBufferPointer++;
mBufferPointer = mBufferPointer % mBuffer.length;
}
/**
* Loads the newValue into the buffer, calculates the average
* and returns that average from this method
*
* This effectively performs low-pass filtering
*/
public boolean getAverage(boolean newValue)
{
//Load the new value into the buffer
put(newValue);
mTrueCount = 0;
for(int x = 0; x < mBuffer.length; x++)
{
if(mBuffer[x])
{
mTrueCount++;
}
}
return mTrueCount > mThreshold;
}
}

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.Listener;
@@ -8,87 +23,86 @@ import java.util.List;
public class CircularBuffer<T> implements Listener<T>
{
private ArrayList<T> mElements;
private Listener<T> mListener;
private int mPointer;
private int mSize;
private List<T> mElements;
private Listener<T> mListener;
private int mPointer;
private int mSize;
/**
* Circular Buffer - stores elements in an array list. Once the buffer is
* full, new samples replace older samples and older samples are broadcast
* to an optional listener.
*
* @param size - size of buffer
*/
public CircularBuffer( int size )
{
mSize = size;
mElements = new ArrayList<T>();
}
/**
* Circular Buffer - stores elements in an array list. Once the buffer is
* full, new samples replace older samples and older samples are broadcast
* to an optional listener.
*
* @param size - size of buffer
*/
public CircularBuffer(int size)
{
mSize = size;
mElements = new ArrayList<T>();
}
/**
* Returns the element stored at index. Note: the index argument refers to
* the internal storage index and does not indicate a temporal relationship
* among elements. For example, if you get indexes 1, 2, and 3, you may be
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
* is currently pointing to and will insert the next element at index 4.
*
* @param index - element index
*
* @return element or null
*/
public T get( int index )
{
if( index < mElements.size() )
{
return mElements.get( index );
}
return null;
}
public int getSize()
{
return mSize;
}
public List<T> getElements()
{
return Collections.unmodifiableList( mElements );
}
@Override
public void receive( T element )
{
T previous = null;
if( mElements.size() > mPointer )
{
previous = mElements.get( mPointer );
mElements.set( mPointer, element );
}
else
{
mElements.add( element );
}
mPointer++;
if( mPointer >= mSize )
{
mPointer = 0;
}
if( previous != null && mListener != null )
{
mListener.receive( previous );
}
}
/**
* Returns the element stored at index. Note: the index argument refers to
* the internal storage index and does not indicate a temporal relationship
* among elements. For example, if you get indexes 1, 2, and 3, you may be
* getting ( oldest - 1, oldest, newest ) elements, if the internal pointer
* is currently pointing to and will insert the next element at index 4.
*
* @param index - element index
* @return element or null
*/
public T get(int index)
{
if(index < mElements.size())
{
return mElements.get(index);
}
public void setListener( Listener<T> listener )
{
mListener = listener;
}
return null;
}
public int getSize()
{
return mSize;
}
public List<T> getElements()
{
return Collections.unmodifiableList(mElements);
}
@Override
public void receive(T element)
{
T previous = null;
if(mElements.size() > mPointer)
{
previous = mElements.get(mPointer);
mElements.set(mPointer, element);
}
else
{
mElements.add(element);
}
mPointer++;
if(mPointer >= mSize)
{
mPointer = 0;
}
if(previous != null && mListener != null)
{
mListener.receive(previous);
}
}
public void setListener(Listener<T> listener)
{
mListener = listener;
}
}

View File

@@ -0,0 +1,124 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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;
/**
* Circular sample buffer - allocates a buffer and stores samples in a circular
* fashion, overwriting older samples with newly arrived samples.
*
* Can be used as a delay-type buffer, to delay samples by the 'size' amount
*/
public class RealCircularBuffer
{
float[] mBuffer;
int mBufferPointer = 0;
public RealCircularBuffer(int size)
{
mBuffer = new float[size];
}
public int getSize()
{
return mBuffer.length;
}
public float[] getBuffer()
{
return mBuffer;
}
/**
* Puts the new value into the buffer and returns the oldest buffer value
* that it replaced
*
* @param newValue
* @return
*/
public float putAndGet(float newValue)
{
float oldestSample = mBuffer[mBufferPointer];
put(newValue);
return oldestSample;
}
/**
* Adds the new value to this buffer.
*/
public void put(float newValue)
{
mBuffer[mBufferPointer] = newValue;
mBufferPointer++;
if(mBufferPointer >= mBuffer.length)
{
mBufferPointer = 0;
}
}
/**
* Returns the sample at the specified index, where index 0 is the oldest sample and index (length - 1) is the
* newest sample.
* @param index in range of 0 to length-1
* @return value at indexed position
*/
public float get(int index)
{
if(index < mBuffer.length)
{
int pointer = mBufferPointer + index;
if(pointer >= mBuffer.length)
{
pointer -= mBuffer.length;
}
return mBuffer[pointer];
}
else
{
throw new ArrayIndexOutOfBoundsException("Index [" + index + "] is not valid for the buffer size of [" +
mBuffer.length + "]");
}
}
/**
* Returns the contents of the circular buffer unwrapped where index 0 is the oldest sample and index length-1
* is the newest sample.
*
* @return array of samples
*/
public float[] get()
{
float[] samples = new float[mBuffer.length];
System.arraycopy(mBuffer, mBufferPointer, samples, 0, mBuffer.length - mBufferPointer);
if(mBufferPointer != 0)
{
System.arraycopy(mBuffer, 0, samples, mBuffer.length - mBufferPointer, mBufferPointer);
}
return samples;
}
}

View File

@@ -123,6 +123,11 @@ public class AttributeMonitor<T>
{
mCurrentValue = null;
mOccurrenceCounts.clear();
if(mListener != null)
{
mListener.receive(new AttributeChangeRequest(mAttribute, null, null));
}
}
/**

View File

@@ -25,32 +25,36 @@ import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.ChannelUtils;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.module.ProcessingChain;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.DefaultTableCellRenderer;
import java.awt.*;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map;
public class ChannelMetadataPanel extends JPanel implements ListSelectionListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelMetadataPanel.class);
private static final String PROPERTY_PREFIX_BACKGROUND = "channel.metadata.panel.state.color.";
private static final String PROPERTY_PREFIX_FOREGROUND = "channel.metadata.panel.text.color.";
private ChannelProcessingManager mChannelProcessingManager;
private IconManager mIconManager;
private JTable mTable;
private Broadcaster<ProcessingChain> mSelectedProcessingChainBroadcaster = new Broadcaster<>();
private Map<State,Color> mBackgroundColors = new HashMap<>();
private Map<State,Color> mForegroundColors = new HashMap<>();
/**
* Table view for currently decoding channel metadata
@@ -94,7 +98,34 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
add(scrollPane);
// new TableColumnWidthMonitor(mTable, PROPERTY_PREFIX, new int[] {15,15,15,15,25,25,25,25,15,40});
setColors();
}
/**
* Setup the background and foreground color palette for the various channel states.
*/
private void setColors()
{
mBackgroundColors.put(State.ACTIVE, Color.CYAN);
mForegroundColors.put(State.ACTIVE, Color.YELLOW);
mBackgroundColors.put(State.CALL, Color.BLUE);
mForegroundColors.put(State.CALL, Color.YELLOW);
mBackgroundColors.put(State.CONTROL, Color.ORANGE);
mForegroundColors.put(State.CONTROL, Color.BLUE);
mBackgroundColors.put(State.DATA, Color.GREEN);
mForegroundColors.put(State.DATA, Color.BLUE);
mBackgroundColors.put(State.ENCRYPTED, Color.MAGENTA);
mForegroundColors.put(State.ENCRYPTED, Color.WHITE);
mBackgroundColors.put(State.FADE, Color.LIGHT_GRAY);
mForegroundColors.put(State.FADE, Color.DARK_GRAY);
mBackgroundColors.put(State.IDLE, Color.WHITE);
mForegroundColors.put(State.IDLE, Color.DARK_GRAY);
mBackgroundColors.put(State.OVERFLOW, Color.RED);
mForegroundColors.put(State.OVERFLOW, Color.YELLOW);
mBackgroundColors.put(State.RESET, Color.PINK);
mForegroundColors.put(State.RESET, Color.YELLOW);
mBackgroundColors.put(State.TEARDOWN, Color.DARK_GRAY);
mForegroundColors.put(State.TEARDOWN, Color.WHITE);
}
@Override
@@ -185,44 +216,14 @@ public class ChannelMetadataPanel extends JPanel implements ListSelectionListene
State state = (State)value;
label.setText(state.getDisplayValue());
switch(state)
if(mBackgroundColors.containsKey(state))
{
case CALL:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "call", Color.BLUE);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "call", Color.YELLOW);
break;
case CONTROL:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "control", Color.ORANGE);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "control", Color.BLUE);
break;
case DATA:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "data", Color.GREEN);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "data", Color.BLUE);
break;
case ENCRYPTED:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "encrypted", Color.MAGENTA);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "encrypted", Color.WHITE);
break;
case FADE:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "fade", Color.LIGHT_GRAY.BLUE);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "fade", Color.YELLOW);
break;
case IDLE:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "idle", Color.WHITE);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "idle", Color.BLUE);
break;
case OVERFLOW:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "overflow", Color.RED);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "overflow", Color.YELLOW);
break;
case RESET:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "reset", Color.PINK);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "reset", Color.YELLOW);
break;
case TEARDOWN:
background = SystemProperties.getInstance().get(PROPERTY_PREFIX_BACKGROUND + "teardown", Color.DARK_GRAY);
foreground = SystemProperties.getInstance().get(PROPERTY_PREFIX_FOREGROUND + "teardown", Color.WHITE);
break;
background = mBackgroundColors.get(state);
}
if(mForegroundColors.containsKey(state))
{
foreground = mForegroundColors.get(state);
}
}
else

View File

@@ -26,11 +26,10 @@ import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.module.decode.event.CallEventPanel;
import io.github.dsheirer.module.decode.event.MessageActivityPanel;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.spectrum.ChannelSpectrumPanel;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
import java.awt.*;
import javax.swing.JPanel;
import java.awt.Color;
public class ChannelMetadataViewer extends JPanel
{
@@ -39,7 +38,6 @@ public class ChannelMetadataViewer extends JPanel
private ChannelDetailPanel mChannelDetailPanel;
private CallEventPanel mCallEventPanel;
private MessageActivityPanel mMessageActivityPanel;
private ChannelSpectrumPanel mChannelSpectrumPanel;
/**
* GUI panel that combines the currently decoding channels metadata table and viewers for channel details,
@@ -51,7 +49,6 @@ public class ChannelMetadataViewer extends JPanel
mChannelDetailPanel = new ChannelDetailPanel(channelProcessingManager);
mCallEventPanel = new CallEventPanel(iconManager);
mMessageActivityPanel = new MessageActivityPanel(channelProcessingManager);
// mChannelSpectrumPanel = new ChannelSpectrumPanel(settingsManager, channelProcessingManager);
mChannelMetadataPanel = new ChannelMetadataPanel(channelProcessingManager, iconManager);
init();

View File

@@ -25,8 +25,6 @@ import io.github.dsheirer.module.decode.DecoderType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledExecutorService;
/**
* Basic decoder channel state - provides the minimum channel state functionality
* to support an always un-squelched audio decoder.
@@ -94,7 +92,7 @@ public class AlwaysUnsquelchedDecoderState extends DecoderState
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
broadcast(new AttributeChangeRequest<String>(Attribute.PRIMARY_ADDRESS_TO, NO_SQUELCH));
broadcast(new DecoderStateEvent(this, Event.ALWAYS_UNSQUELCH, State.IDLE));

View File

@@ -20,8 +20,6 @@ package io.github.dsheirer.channel.state;
import io.github.dsheirer.audio.squelch.ISquelchStateProvider;
import io.github.dsheirer.audio.squelch.SquelchState;
import io.github.dsheirer.channel.heartbeat.Heartbeat;
import io.github.dsheirer.channel.heartbeat.IHeartbeatListener;
import io.github.dsheirer.channel.metadata.Attribute;
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
import io.github.dsheirer.channel.metadata.IAttributeChangeRequestListener;
@@ -34,20 +32,19 @@ import io.github.dsheirer.module.decode.DecoderType;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.event.CallEvent;
import io.github.dsheirer.module.decode.event.ICallEventProvider;
import io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.IOverflowListener;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeListener;
import io.github.dsheirer.source.ISourceEventListener;
import io.github.dsheirer.source.ISourceEventProvider;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.heartbeat.Heartbeat;
import io.github.dsheirer.source.heartbeat.IHeartbeatListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ScheduledExecutorService;
import static io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event.NOTIFICATION_FREQUENCY_CHANGE;
public class ChannelState extends Module implements ICallEventProvider, IDecoderStateEventListener,
IDecoderStateEventProvider, IFrequencyChangeListener, IHeartbeatListener, ISquelchStateProvider,
IAttributeChangeRequestListener, IOverflowListener
IDecoderStateEventProvider, ISourceEventListener, ISourceEventProvider, IHeartbeatListener,
ISquelchStateProvider, IAttributeChangeRequestListener, IOverflowListener
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelState.class);
@@ -59,12 +56,13 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
private Listener<CallEvent> mCallEventListener;
private Listener<DecoderStateEvent> mDecoderStateListener;
private Listener<SquelchState> mSquelchStateListener;
private Listener<SourceEvent> mExternalSourceEventListener;
private DecoderStateEventReceiver mDecoderStateEventReceiver = new DecoderStateEventReceiver();
private HeartbeatReceiver mHeartbeatReceiver = new HeartbeatReceiver();
private ChannelType mChannelType;
private TrafficChannelManager mTrafficChannelEndListener;
private CallEvent mTrafficChannelCallEvent;
private FrequencyChangeListener mFrequencyChangeListener;
private SourceEventListener mInternalSourceEventListener;
private boolean mSquelchLocked = false;
private boolean mSelected = false;
@@ -124,7 +122,7 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
mMutableMetadata.receive(new AttributeChangeRequest<State>(Attribute.CHANNEL_STATE, mState));
@@ -150,6 +148,8 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
mTrafficChannelCallEvent = null;
mSquelchLocked = false;
setState(State.TEARDOWN);
}
public void dispose()
@@ -160,14 +160,14 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
@Override
public Listener<FrequencyChangeEvent> getFrequencyChangeListener()
public Listener<SourceEvent> getSourceEventListener()
{
if(mFrequencyChangeListener == null)
if(mInternalSourceEventListener == null)
{
mFrequencyChangeListener = new FrequencyChangeListener();
mInternalSourceEventListener = new SourceEventListener();
}
return mFrequencyChangeListener;
return mInternalSourceEventListener;
}
private boolean isStandardChannel()
@@ -257,6 +257,17 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
}
/**
* Broadcasts the source event to a registered external source event listener
*/
protected void broadcast(SourceEvent sourceEvent)
{
if(mExternalSourceEventListener != null)
{
mExternalSourceEventListener.receive(sourceEvent);
}
}
/**
* Sets the squelch state listener
*/
@@ -294,6 +305,11 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
{
switch(state)
{
case ACTIVE:
broadcast(SquelchState.SQUELCH);
updateFadeTimeout();
mState = state;
break;
case CONTROL:
//Don't allow traffic channels to be control channels, otherwise they can't transition to teardown
if(isStandardChannel())
@@ -518,18 +534,46 @@ public class ChannelState extends Module implements ICallEventProvider, IDecoder
}
/**
* Listener to receive frequency change events and rebroadcast them to the decoder states
* Registers the listener to receive source events from the channel state
*/
public class FrequencyChangeListener implements Listener<FrequencyChangeEvent>
@Override
public void setSourceEventListener(Listener<SourceEvent> listener)
{
mExternalSourceEventListener = listener;
}
/**
* De-Registers a listener from receiving source events from the channel state
*/
@Override
public void removeSourceEventListener()
{
mExternalSourceEventListener = null;
}
/**
* Listener to receive source events.
*/
public class SourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(FrequencyChangeEvent frequencyChangeEvent)
public void receive(SourceEvent sourceEvent)
{
if(frequencyChangeEvent.getEvent() == NOTIFICATION_FREQUENCY_CHANGE)
switch(sourceEvent.getEvent())
{
long frequency = frequencyChangeEvent.getValue().longValue();
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
case NOTIFICATION_FREQUENCY_CHANGE:
//Rebroadcast source frequency change events for the decoder(s) to process
long frequency = sourceEvent.getValue().longValue();
broadcast(new DecoderStateEvent(this, Event.SOURCE_FREQUENCY, getState(), frequency));
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR:
//Rebroadcast frequency error measurements to external listener if we're currently
//in an active (ie sync locked) state.
if(getState().isActiveState())
{
broadcast(SourceEvent.frequencyErrorMeasurementSyncLocked(sourceEvent.getValue().longValue(), mChannelType.name()));
}
break;
}
}
}

View File

@@ -24,54 +24,114 @@ import java.util.EnumSet;
*/
public enum State
{
IDLE("IDLE")
/**
* Active state indicates that valid messages are being decoded that do not indicate any call or data
* activity. For example, this is used for P25 when a channel is actively transmitting Terminator Data
* Units in between data packets or at the beginning or end of a call.
*/
ACTIVE("ACTIVE")
{
@Override
public boolean canChangeTo(State state)
{
return state != TEARDOWN && state != RESET && state != OVERFLOW;
return state == CALL |
state == CONTROL ||
state == DATA ||
state == ENCRYPTED ||
state == FADE ||
state == TEARDOWN;
}
},
/**
* Indicates that call messages are being decoded, or a call is in progress (ie producing audio).
*/
CALL("CALL")
{
@Override
public boolean canChangeTo(State state)
{
return state == CONTROL || state == DATA || state == ENCRYPTED || state == FADE || state == TEARDOWN;
}
},
DATA("DATA")
{
@Override
public boolean canChangeTo(State state)
{
return state == CALL || state == CONTROL || state == ENCRYPTED || state == FADE || state == TEARDOWN;
}
},
ENCRYPTED("ENCRYPTED")
{
@Override
public boolean canChangeTo(State state)
{
return state == FADE || state == TEARDOWN;
return state == ACTIVE ||
state == CONTROL ||
state == DATA ||
state == ENCRYPTED ||
state == FADE ||
state == TEARDOWN;
}
},
/**
* Indicates a trunking control channel is being decoded.
*/
CONTROL("CONTROL")
{
@Override
public boolean canChangeTo(State state)
{
return state == IDLE || state == FADE;
return state == IDLE ||
state == FADE;
}
},
/**
* Indicates data packets are being decoded
*/
DATA("DATA")
{
@Override
public boolean canChangeTo(State state)
{
return state == ACTIVE ||
state == CALL ||
state == CONTROL ||
state == ENCRYPTED ||
state == FADE ||
state == TEARDOWN;
}
},
/**
* Indicates encrypted audio is detected.
*/
ENCRYPTED("ENCRYPTED")
{
@Override
public boolean canChangeTo(State state)
{
return state == FADE ||
state == TEARDOWN;
}
},
/**
* Fade state occurs when the channel is no longer actively decoding an messages. This is a transitory
* state before transition to IDLE or back to an active state.
*/
FADE("FADE")
{
@Override
public boolean canChangeTo(State state)
{
return state != FADE && state != RESET && state != OVERFLOW;
return state != FADE &&
state != RESET &&
state != OVERFLOW;
}
},
/**
* Indicates the channel is idle and not actively decoding any messages
*/
IDLE("IDLE")
{
@Override
public boolean canChangeTo(State state)
{
return state != TEARDOWN &&
state != RESET &&
state != OVERFLOW;
}
},
/**
* Indicates that the channel's sample buffer has overflowed and new buffer samples are no longer being
* delivered to the channel. This state persists until the processing catches up and reduces the number
* of samples in the input buffer below a threshold value. Transition to and from this state is exclusively
* managed by the Channel State and no other channel states can be processed from the channel decoders
* until the channel state is taken out of overflow.
*/
OVERFLOW("OVERFLOW")
{
@Override
@@ -80,6 +140,9 @@ public enum State
return false;
}
},
/**
* Indicates that the channel has been reset and is ready to use for a new decoding session.
*/
RESET("RESET")
{
@Override
@@ -88,6 +151,10 @@ public enum State
return state == IDLE;
}
},
/**
* Call or data teardown. This state is used by traffic channels to indicate that a call or data session
* has concluded and the channel is ready for tear down and reuse.
*/
TEARDOWN("TEARDOWN")
{
@Override
@@ -99,7 +166,7 @@ public enum State
private String mDisplayValue;
public static final EnumSet<State> CALL_STATES = EnumSet.of(CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> CALL_STATES = EnumSet.of(ACTIVE, CALL, CONTROL, DATA, ENCRYPTED);
public static final EnumSet<State> IDLE_STATES = EnumSet.of(IDLE, FADE);
private State(String displayValue)
@@ -109,6 +176,17 @@ public enum State
public abstract boolean canChangeTo(State state);
/**
* Indicates that this state is an active (ie decoding) state and is one of the states
* enumerated in the CALL_STATES enumeration set.
*
* @return true if this state is an active state
*/
public boolean isActiveState()
{
return CALL_STATES.contains(this);
}
public String getDisplayValue()
{
return mDisplayValue;

View File

@@ -1,19 +1,16 @@
/*******************************************************************************
* sdrtrunk
* sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.channel.traffic;
@@ -29,13 +26,12 @@ import io.github.dsheirer.controller.channel.TrafficChannelEvent;
import io.github.dsheirer.module.Module;
import io.github.dsheirer.module.decode.config.DecodeConfiguration;
import io.github.dsheirer.module.decode.event.CallEvent;
import io.github.dsheirer.module.decode.event.CallEvent.CallEventType;
import io.github.dsheirer.module.decode.event.ICallEventProvider;
import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.tuner.TunerChannel;
import io.github.dsheirer.source.tuner.TunerChannel.Type;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -43,7 +39,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
public class TrafficChannelManager extends Module implements ICallEventProvider, IDecoderStateEventListener
{
@@ -61,7 +56,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
private ChannelModel mChannelModel;
private DecodeConfiguration mDecodeConfiguration;
private EventLogConfiguration mEventLogConfiguration;
private RecordConfiguration mRecordConfiguration;
private String mSystem;
private String mSite;
@@ -75,7 +69,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
* @param channelModel containing channels currently in use
* @param decodeConfiguration - decoder configuration to use for each
* traffic channel allocation.
* @param eventLogConfiguration - logging configuration to use for traffic channels
* @param recordConfiguration - recording options for each traffic channel
* @param system label to use to describe the system
* @param site label to use to describe the site
@@ -85,7 +78,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
*/
public TrafficChannelManager(ChannelModel channelModel,
DecodeConfiguration decodeConfiguration,
EventLogConfiguration eventLogConfiguration,
RecordConfiguration recordConfiguration,
String system,
String site,
@@ -94,7 +86,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
mChannelModel = channelModel;
mDecodeConfiguration = decodeConfiguration;
mEventLogConfiguration = eventLogConfiguration;
mRecordConfiguration = recordConfiguration;
mSystem = system;
mSite = site;
@@ -150,8 +141,6 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
channel.setDecodeConfiguration(mDecodeConfiguration);
channel.setEventLogConfiguration(mEventLogConfiguration);
channel.setRecordConfiguration(mRecordConfiguration);
channel.setAliasListName(mAliasListName);
@@ -161,7 +150,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
mTrafficChannelPool.add(channel);
}
/* If we have a configured channel, update metadata */
/* If we have a configured channel, update metadata */
if(channel != null)
{
channel.setSourceConfiguration(new SourceConfigTuner(tunerChannel));
@@ -184,7 +173,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
CallEvent callEvent = event.getCallEvent();
/* Check for duplicate events and suppress */
/* Check for duplicate events and suppress */
synchronized(mTrafficChannelsInUse)
{
if(mTrafficChannelsInUse.containsKey(callEvent.getChannel()))
@@ -196,8 +185,8 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
if(frequency > 0)
{
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(Type.TRAFFIC,
frequency, mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
Channel channel = getChannel(callEvent.getChannel(), new TunerChannel(frequency,
mDecodeConfiguration.getDecoderType().getChannelBandwidth()));
if(channel != null)
{
@@ -213,7 +202,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -230,7 +219,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -247,7 +236,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
else
{
callEvent.setCallEventType(CallEvent.CallEventType.CALL_DETECT);
callEvent.setCallEventType(CallEventType.CALL_DETECT);
String details = callEvent.getDetails();
@@ -337,7 +326,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
@Override
public void start(ScheduledExecutorService executor)
public void start()
{
}
@@ -348,7 +337,7 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
{
List<String> channels = new ArrayList<>();
/* Copy the keyset so we don't get concurrent modification of the map */
/* Copy the keyset so we don't get concurrent modification of the map */
channels.addAll(mTrafficChannelsInUse.keySet());
for(String channel : channels)
@@ -399,4 +388,4 @@ public class TrafficChannelManager extends Module implements ICallEventProvider,
}
}
}
}
}

View File

@@ -31,18 +31,16 @@ import io.github.dsheirer.module.log.config.EventLogConfiguration;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.config.RecordConfiguration;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceType;
import io.github.dsheirer.source.config.SourceConfigFactory;
import io.github.dsheirer.source.config.SourceConfigRecording;
import io.github.dsheirer.source.config.SourceConfigTuner;
import io.github.dsheirer.source.config.SourceConfiguration;
import io.github.dsheirer.source.tuner.TunerChannel;
import io.github.dsheirer.source.tuner.TunerChannel.Type;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
@JacksonXmlRootElement(localName = "channel")
public class Channel extends Configuration implements Listener<FrequencyChangeEvent>
public class Channel extends Configuration implements Listener<SourceEvent>
{
// Standard channels are persisted and traffic channels are temporary
public enum ChannelType
@@ -490,15 +488,14 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
{
SourceConfigTuner config = (SourceConfigTuner)mSourceConfiguration;
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
mTunerChannel = new TunerChannel(config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
}
else if(mSourceConfiguration.getSourceType() == SourceType.RECORDING)
{
SourceConfigRecording config =
(SourceConfigRecording)mSourceConfiguration;
SourceConfigRecording config = (SourceConfigRecording)mSourceConfiguration;
mTunerChannel = new TunerChannel(Type.LOCKED, config.getFrequency(),
mTunerChannel = new TunerChannel(config.getFrequency(),
mDecodeConfiguration.getDecoderType().getChannelBandwidth());
}
}
@@ -524,9 +521,9 @@ public class Channel extends Configuration implements Listener<FrequencyChangeEv
* be used to visually show in the spectral display.
*/
@Override
public void receive(FrequencyChangeEvent event)
public void receive(SourceEvent event)
{
if(event.getEvent() == Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
if(event.getEvent() == SourceEvent.Event.NOTIFICATION_CHANNEL_FREQUENCY_CORRECTION_CHANGE)
{
mChannelFrequencyCorrection = event.getValue().intValue();
}

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.controller.channel;
import com.jidesoft.swing.JideSplitPane;
@@ -24,8 +39,7 @@ import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ChannelController extends JPanel
implements ActionListener, ListSelectionListener
public class ChannelController extends JPanel implements ActionListener, ListSelectionListener
{
private static final long serialVersionUID = 1L;
@@ -168,9 +182,13 @@ public class ChannelController extends JPanel
if( index >= 0 )
{
int translatedIndex = mChannelTable.convertRowIndexToView( index );
mChannelTable.setRowSelectionInterval( translatedIndex, translatedIndex );
mChannelTable.scrollRectToVisible(
new Rectangle( mChannelTable.getCellRect( translatedIndex, 0, true ) ) );
if(translatedIndex >= 0)
{
mChannelTable.setRowSelectionInterval( translatedIndex, translatedIndex );
mChannelTable.scrollRectToVisible(
new Rectangle( mChannelTable.getCellRect( translatedIndex, 0, true ) ) );
}
}
}

View File

@@ -19,7 +19,6 @@
package io.github.dsheirer.controller.channel;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioPacket;
import io.github.dsheirer.channel.metadata.Attribute;
import io.github.dsheirer.channel.metadata.AttributeChangeRequest;
import io.github.dsheirer.channel.metadata.ChannelMetadataModel;
@@ -34,7 +33,9 @@ import io.github.dsheirer.module.decode.event.MessageActivityModel;
import io.github.dsheirer.module.log.EventLogManager;
import io.github.dsheirer.record.RecorderManager;
import io.github.dsheirer.record.RecorderType;
import io.github.dsheirer.record.binary.BinaryRecorder;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableAudioPacket;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.SourceManager;
@@ -54,7 +55,7 @@ public class ChannelProcessingManager implements ChannelEventListener
private Map<Integer,ProcessingChain> mProcessingChains = new HashMap<>();
private List<Listener<AudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
private List<Listener<ReusableAudioPacket>> mAudioPacketListeners = new CopyOnWriteArrayList<>();
private List<Listener<Message>> mMessageListeners = new CopyOnWriteArrayList<>();
private ChannelModel mChannelModel;
@@ -211,7 +212,8 @@ public class ChannelProcessingManager implements ChannelEventListener
try
{
source = mSourceManager.getSource(channel.getSourceConfiguration(),
channel.getDecodeConfiguration().getDecoderType().getChannelBandwidth());
channel.getDecodeConfiguration().getDecoderType().getChannelBandwidth(),
channel.getDecodeConfiguration().getDecoderType().getChannelSpecification());
}
catch(SourceException se)
{
@@ -232,7 +234,7 @@ public class ChannelProcessingManager implements ChannelEventListener
processingChain = new ProcessingChain(channel.getChannelType());
/* Register global listeners */
for(Listener<AudioPacket> listener : mAudioPacketListeners)
for(Listener<ReusableAudioPacket> listener : mAudioPacketListeners)
{
processingChain.addAudioPacketListener(listener);
}
@@ -308,6 +310,25 @@ public class ChannelProcessingManager implements ChannelEventListener
{
processingChain.addModule(mRecorderManager.getBasebandRecorder(channel.toString()));
}
/* Add decoded bit stream recorder if the decoder supports bitstream output */
if(DecoderFactory.getBitstreamDecoders().contains(channel.getDecodeConfiguration().getDecoderType()))
{
if((recorders.contains(RecorderType.DEMODULATED_BIT_STREAM) &&
channel.getChannelType() == Channel.ChannelType.STANDARD))
{
processingChain.addModule(new BinaryRecorder(mRecorderManager.getRecordingBasePath(),
channel.toString()));
}
/* Add traffic channel decoded bit stream recorder */
if(recorders.contains(RecorderType.TRAFFIC_DEMODULATED_BIT_STREAM) &&
channel.getChannelType() == Channel.ChannelType.TRAFFIC)
{
processingChain.addModule(new BinaryRecorder(mRecorderManager.getRecordingBasePath(),
channel.toString()));
}
}
}
processingChain.setSource(source);
@@ -363,11 +384,27 @@ public class ChannelProcessingManager implements ChannelEventListener
}
}
/**
* Stops all currently processing channels to prepare for shutdown.
*/
public void shutdown()
{
mLog.debug("Stopping Channels ...");
for(Channel channel: mChannelModel.getChannels())
{
if(channel.isProcessing())
{
mLog.debug("Stopping channel: " + channel.toString());
stopProcessing(channel, true);
}
}
}
/**
* Adds a message listener that will be added to all channels to receive
* any messages.
*/
public void addAudioPacketListener(Listener<AudioPacket> listener)
public void addAudioPacketListener(Listener<ReusableAudioPacket> listener)
{
mAudioPacketListeners.add(listener);
}
@@ -375,7 +412,7 @@ public class ChannelProcessingManager implements ChannelEventListener
/**
* Removes a message listener.
*/
public void removeAudioPacketListener(Listener<AudioPacket> listener)
public void removeAudioPacketListener(Listener<ReusableAudioPacket> listener)
{
mAudioPacketListeners.remove(listener);
}

View File

@@ -1,77 +1,76 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp;
import io.github.dsheirer.bits.IBinarySymbolProcessor;
import io.github.dsheirer.sample.Listener;
public class NRZDecoder implements Listener<Boolean>
public class NRZDecoder implements IBinarySymbolProcessor
{
public final static boolean MODE_NORMAL = true;
public final static boolean MODE_INVERTED = false;
private Listener<Boolean> mListener;
private boolean mMode = MODE_NORMAL;
private boolean mPrevious;
public final static boolean MODE_NORMAL = true;
public final static boolean MODE_INVERTED = false;
/**
* Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
*
* Use default constructor for NRZ output. Use the alternate constructor
* with the MODE_INVERTED argument for NRZ-I output.
*
* Performs XOR of incoming bit with previous output and sends the result
* to the registered listener.
*/
public NRZDecoder( boolean mode )
{
mMode = mode;
}
public NRZDecoder()
{
this( MODE_NORMAL );
}
public void dispose()
{
mListener = null;
}
private IBinarySymbolProcessor mListener;
private boolean mMode = MODE_NORMAL;
private boolean mPrevious;
@Override
public void receive( Boolean bit )
/**
* Non Return To Zero (NRZ) and NRZ Inverted (NRZ-I) Decoder.
*
* Use default constructor for NRZ output. Use the alternate constructor
* with the MODE_INVERTED argument for NRZ-I output.
*
* Performs XOR of incoming bit with previous output and sends the result
* to the registered listener.
*/
public NRZDecoder(boolean mode)
{
boolean result = mPrevious ^ bit;
if( mListener != null )
{
mListener.receive( mMode ? result : !result );
}
mPrevious = result;
mMode = mode;
}
public void setListener( Listener<Boolean> listener )
{
mListener = listener;
}
public void removeListener( Listener<Boolean> listener )
{
mListener = null;
}
public NRZDecoder()
{
this(MODE_NORMAL);
}
public void dispose()
{
mListener = null;
}
@Override
public void receive(boolean symbol)
{
boolean result = mPrevious ^ symbol;
if(mListener != null)
{
mListener.receive(mMode ? result : !result);
}
mPrevious = result;
}
public void setListener(IBinarySymbolProcessor listener)
{
mListener = listener;
}
public void removeListener(Listener<Boolean> listener)
{
mListener = null;
}
}

View File

@@ -0,0 +1,252 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import io.github.dsheirer.bits.IBinarySymbolProcessor;
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.filter.resample.RealResampler;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Audio Frequency Shift Keying (AFSK) 1200-baud correlation decoder for decoding 8 kHz FM-demodulated audio samples.
*
* Note: internally the incoming 8 kHz sample rate is resampled to 7200 Hz to be an integral of the 1200 Hz baud rate
* which equates to six samples per symbol. Each symbol is correlated over an eight sample period with correlation
* values averaged over a seven correlation value period.
*
* Provides normal or inverted decoded output.
*/
public class AFSK1200Decoder implements Listener<ReusableFloatBuffer>
{
private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class);
public enum Output
{
NORMAL, INVERTED
}
public static final double SAMPLE_RATE = 7200.0d;
public static final int SAMPLES_PER_SYMBOL = 6; //SAMPLE_RATE / 1200 baud;
//Correlation period of 8 samples works well and should align nicely with SIMD intrinsic vector sizes
public static final int CORRELATION_PERIOD = SAMPLES_PER_SYMBOL + 2;
public static final int AVERAGING_PERIOD = SAMPLES_PER_SYMBOL + 1;
public static final double MARK = 1200.0;
public static final double SPACE = 1800.0;
public static final float TIMING_ERROR_GAIN = 1.0f / 3.0f; //Timing error adjustments over 3 symbol periods
private Correlator mCorrelatorMark = new Correlator(SAMPLE_RATE, MARK, AVERAGING_PERIOD, CORRELATION_PERIOD);
private Correlator mCorrelatorSpace = new Correlator(SAMPLE_RATE, SPACE, AVERAGING_PERIOD, CORRELATION_PERIOD);
private float[] mCorrelationValuesMark;
private float[] mCorrelationValuesSpace;
protected boolean mNormalOutput;
protected float mSymbolTimingGain = TIMING_ERROR_GAIN;
protected AFSKSampleBuffer mSampleBuffer;
protected AFSKTimingErrorDetector mTimingErrorDetector = new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL);
protected IBinarySymbolProcessor mBinarySymbolProcessor;
private boolean mSampleDecision;
//Resample to an integral of the baud rate 1200 baud * 6 samples per symbol = 7200.0 Hertz
private RealResampler mResampler = new RealResampler(8000.0, SAMPLE_RATE, 2000, 1);
/**
* Constructs a decoder using the provided arguments.
*
* @param sampleBuffer for storing incoming samples and calculating symbols
* @param detector for timing error alignment
* @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa)
*/
public AFSK1200Decoder(AFSKSampleBuffer sampleBuffer, AFSKTimingErrorDetector detector, Output output)
{
mTimingErrorDetector = detector;
mSampleBuffer = sampleBuffer;
mSampleBuffer.setTimingGain(mSymbolTimingGain);
mResampler.setListener(new Decoder());
mNormalOutput = (output == Output.NORMAL);
}
/**
* Constructs a decoder using the provided arguments.
*
* @param output NORMAL: 1200Hz = Mark(1) and 1800Hz = Space(0), or INVERTED (vice-versa)
*/
public AFSK1200Decoder(Output output)
{
this(new AFSKSampleBuffer(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN),
new AFSKTimingErrorDetector(SAMPLES_PER_SYMBOL), output);
}
/**
* Disposes of all references to prepare for garbage collection
*/
public void dispose()
{
}
/**
* Processes the buffer samples by converting all samples to boolean values reflecting if the sample value is
* greater than zero (or not). Average symbol timing offset is calculated for the full buffer and the offset is
* adjusted and then each of the symbols are decoded using a simple majority decision.
*
* @param buffer containing 8.0 kHz unfiltered FM demodulated audio samples with sub-audible LTR signalling.
*/
@Override
public void receive(ReusableFloatBuffer buffer)
{
mResampler.resample(buffer);
}
protected void dispatch(boolean symbol)
{
if(mBinarySymbolProcessor != null)
{
mBinarySymbolProcessor.receive(mNormalOutput ? symbol : !symbol);
}
}
/**
* Registers a listener to receive decoded LTR symbols.
*
* @param binarySymbolProcessor to receive symbols.
*/
public void setSymbolProcessor(IBinarySymbolProcessor binarySymbolProcessor)
{
mBinarySymbolProcessor = binarySymbolProcessor;
}
/**
* Removes the symbol listener from receiving decoded LTR symbols.
*/
public void removeListener()
{
mBinarySymbolProcessor = null;
}
public class Decoder implements Listener<ReusableFloatBuffer>
{
@Override
public void receive(ReusableFloatBuffer buffer)
{
//Calculate correlation values against each 1200/1800 reference signal
mCorrelationValuesMark = mCorrelatorMark.process(buffer);
mCorrelationValuesSpace = mCorrelatorSpace.process(buffer);
buffer.decrementUserCount();
for(int x = 0; x < mCorrelationValuesMark.length; x++)
{
//1200 = Mark (1) and 1800 = Space (0)
mSampleDecision = mCorrelationValuesMark[x] > mCorrelationValuesSpace[x];
mSampleBuffer.receive(mSampleDecision);
mTimingErrorDetector.receive(mSampleDecision);
if(mSampleBuffer.hasSymbol())
{
dispatch(mSampleBuffer.getSymbol());
mSampleBuffer.resetAndAdjust(mTimingErrorDetector.getError());
}
}
}
}
/**
* Generates a correlation value for each FM demodulated sample against samples generated for an established
* reference frequency, correlated and averaged over separate periods relative to the samples per symbol period.
*
* The sample rate of the correlator should be an integral value of the target frequency for optimal performance,
* This correlator only works across integral sample periods and does not provide intra-sample interpolation.
*
* Correlation period (ie number of samples) should be relative to the samples per symbol. You may have to
* experiment with various values of (samplesPerSymbol +/- 1, 2 or 3) to obtain the optimal decoder performance.
*
* Averaging period defines the number of correlation values to average before producing the final correlation
* value for each sample.
*/
public class Correlator
{
private FloatAveragingBuffer mAveragingBuffer;
private float[] mReferenceSamples;
private float[] mDemodulatedSamples;
private float[] mCorrelationValues;
private float mCorrelationAccumulator;
/**
* Constructs a correlator instance,
*
* @param sampleRate of the incoming sample stream. Note: this should be an integral of the symbol rate.
* @param frequency of the mark or space symbol to test for correlation
* @param averagingPeriod is the number of correlation values to average each period
* @param correlationPeriod is the number of samples to correlate each period
*/
public Correlator(double sampleRate, double frequency, int averagingPeriod, int correlationPeriod)
{
mAveragingBuffer = new FloatAveragingBuffer(averagingPeriod);
IOscillator referenceSignalGenerator = new Oscillator(frequency, sampleRate);
mReferenceSamples = referenceSignalGenerator.generateReal(correlationPeriod);
mDemodulatedSamples = new float[correlationPeriod];
}
/**
* Processes each sample in the incoming sample buffer against a generated reference sample set for one symbol
* period to derive a correlation value that is in-turn averaged over one symbol period.
*
* @param reusableFloatBuffer containing FM demodulated samples
* @return a reusable array of correlation values for each sample
*/
public float[] process(ReusableFloatBuffer reusableFloatBuffer)
{
if(mCorrelationValues == null || mCorrelationValues.length != reusableFloatBuffer.getSampleCount())
{
mCorrelationValues = new float[reusableFloatBuffer.getSampleCount()];
}
float[] samples = reusableFloatBuffer.getSamples();
int y;
for(int x = 0; x < samples.length; x++)
{
//Note: we're using array copy and dot product structures that the JRE can promote to SIMD intrinsics
//when the host processor supports SIMD instructions
System.arraycopy(mDemodulatedSamples, 1, mDemodulatedSamples, 0, mDemodulatedSamples.length - 1);
mDemodulatedSamples[mDemodulatedSamples.length - 1] = samples[x];
mCorrelationAccumulator = 0.0f;
for(y = 0; y < mDemodulatedSamples.length; y++)
{
mCorrelationAccumulator += mDemodulatedSamples[y] * mReferenceSamples[y];
}
//Add the absolute value of correlation accumulator value to the averaging buffer and store the current
//average as the correlation value for this sample. We use absolute value because we don't care if the
//signal is out of phase with the reference samples
mCorrelationValues[x] = mAveragingBuffer.get(Math.abs(mCorrelationAccumulator));
}
return mCorrelationValues;
}
}
}

View File

@@ -0,0 +1,51 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
public class AFSK1200DecoderInstrumented extends AFSK1200Decoder
{
private Broadcaster<Boolean> mSymbolBroadcaster = new Broadcaster<>();
public AFSK1200DecoderInstrumented(boolean invertOutput)
{
super(new AFSKSampleBufferInstrumented(SAMPLES_PER_SYMBOL, TIMING_ERROR_GAIN),
new AFSKTimingErrorDetectorInstrumented(SAMPLES_PER_SYMBOL), Output.NORMAL);
}
public AFSKSampleBufferInstrumented getSampleBuffer()
{
return (AFSKSampleBufferInstrumented)mSampleBuffer;
}
public AFSKTimingErrorDetectorInstrumented getErrorDetector()
{
return (AFSKTimingErrorDetectorInstrumented)mTimingErrorDetector;
}
protected void dispatch(boolean symbol)
{
super.dispatch(symbol);
mSymbolBroadcaster.broadcast(symbol);
}
public void addListener(Listener<Boolean> listener)
{
mSymbolBroadcaster.addListener(listener);
}
}

View File

@@ -0,0 +1,121 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AFSKSampleBuffer
{
private final static Logger mLog = LoggerFactory.getLogger(AFSKSampleBuffer.class);
private final static float SYMBOL_SAMPLE_THRESHOLD = 1.0f;
protected boolean[] mDelayLine;
protected int mDelayLinePointer = 0;
private int mHalfIntegerSamplesPerSymbol;
private int mPositiveSampleDecisionCount;
protected int mTwiceSamplesPerSymbol;
protected int mSymbolStart;
protected int mSymbolEnd;
protected int mSamplesPerSymbol;
protected float mSymbolSamplingPoint;
protected float mSymbolTimingGain;
/**
* Buffer to store complex sample data and produce interpolated samples.
* @param samplesPerSymbol
*/
public AFSKSampleBuffer(int samplesPerSymbol, float symbolTimingGain)
{
mSamplesPerSymbol = samplesPerSymbol;
mSymbolTimingGain = symbolTimingGain;
mHalfIntegerSamplesPerSymbol = samplesPerSymbol / 2;
mSymbolSamplingPoint = samplesPerSymbol;
mTwiceSamplesPerSymbol = 2 * samplesPerSymbol;
mDelayLine = new boolean[2 * mTwiceSamplesPerSymbol];
}
/**
* Stores the sample in the sample buffer to support making symbol decisions.
*
* Note: use the hasSymbol() method to determine if sufficient samples have been collected to make a symbol
* determination.
*
* @param sample to store in the sample buffer
*/
public void receive(boolean sample)
{
mSymbolSamplingPoint--;
mDelayLine[mDelayLinePointer] = sample;
mDelayLine[mDelayLinePointer + mTwiceSamplesPerSymbol] = sample;
//Increment pointer and keep pointer in bounds
mDelayLinePointer++;
mDelayLinePointer = mDelayLinePointer % mTwiceSamplesPerSymbol;
}
/**
* Adjusts samples per symbol and symbol timing counters and increments the sample counter to collect another
* symbol.
*
* @param symbolTimingError from a symbol timing error detector
*/
public void resetAndAdjust(float symbolTimingError)
{
mSymbolSamplingPoint += (mSamplesPerSymbol + (symbolTimingError * mSymbolTimingGain));
}
public void setTimingGain(float gain)
{
mSymbolTimingGain = gain;
}
/**
* Indicates if this buffer has accumulated enough samples to represent a full symbol
*/
public boolean hasSymbol()
{
return mSymbolSamplingPoint < SYMBOL_SAMPLE_THRESHOLD;
}
/**
* Calculates the symbol as a majority decision where the number of positive/true samples are greater than half
* of the samples per symbol. Symbol decision is based on the samples collected over the preceeding 1.0 to 2.0
* symbol periods.
*
* @return a true symbol if the majority of the samples were true or positive, otherwise a false symbol.
*/
public boolean getSymbol()
{
mPositiveSampleDecisionCount = 0;
mSymbolStart = mDelayLinePointer;
mSymbolEnd = mDelayLinePointer + mSamplesPerSymbol - 1;
for(int x = mSymbolStart; x <= mSymbolEnd; x++)
{
if(mDelayLine[x])
{
mPositiveSampleDecisionCount++;
}
}
return mPositiveSampleDecisionCount > mHalfIntegerSamplesPerSymbol;
}
}

View File

@@ -0,0 +1,90 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
public class AFSKSampleBufferInstrumented extends AFSKSampleBuffer
{
public boolean mLastSymbol;
/**
* Buffer to store complex sample data and produce interpolated samples.
*
* @param samplesPerSymbol
* @param symbolTimingGain
*/
public AFSKSampleBufferInstrumented(int samplesPerSymbol, float symbolTimingGain)
{
super(samplesPerSymbol, symbolTimingGain);
}
/**
* Sample delay buffer containing boolean (ie positive or negative) samples.
*/
public boolean[] getDelayLine()
{
return mDelayLine;
}
/**
* Current delay buffer pointer which points to where the next sample will be stored and/or the oldest sample in
* the buffer.
*/
public int getDelayLinePointer()
{
return mDelayLinePointer;
}
public int getDelayLineSecondPointer()
{
return mDelayLinePointer + mTwiceSamplesPerSymbol;
}
/**
* Current symbol timing gain value
* @return
*/
public float getSymbolTimingGain()
{
return mSymbolTimingGain;
}
public float getSamplesPerSymbol()
{
return mSamplesPerSymbol;
}
@Override
public boolean getSymbol()
{
mLastSymbol = super.getSymbol();
return mLastSymbol;
}
public boolean getLastSymbol()
{
return mLastSymbol;
}
public int getSymbolStart()
{
return mSymbolStart;
}
public int getSymbolEnd()
{
return mSymbolEnd;
}
}

View File

@@ -0,0 +1,122 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import io.github.dsheirer.buffer.FloatAveragingBuffer;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.complex.Complex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.DecimalFormat;
public class AFSKTest
{
private final static Logger mLog = LoggerFactory.getLogger(AFSK1200Decoder.class);
private static final DecimalFormat df = new DecimalFormat("00.000");
public static String format(double value)
{
if(value > 0)
{
return " " + df.format(value);
}
else
{
return df.format(value);
}
}
public static float[] generateAFSK1200(boolean[] symbols, int samplesPerSymbol, float gain)
{
float[] samples = new float[symbols.length * samplesPerSymbol];
Oscillator oscillator = new Oscillator(1200.0, (double)samplesPerSymbol * 1200);
for(int x = 0; x < symbols.length; x++)
{
oscillator.setFrequency(symbols[x] ? 1200.0 : 1800.0);
for(int y = 0; y < samplesPerSymbol; y++)
{
Complex sample = new Complex(oscillator.inphase(), oscillator.quadrature());
sample.multiply(gain);
samples[x * samplesPerSymbol + y] = sample.inphase();
oscillator.rotate();
}
}
return samples;
}
public static void main(String[] args)
{
int sampleSize = 6;
FloatAveragingBuffer avg1200 = new FloatAveragingBuffer(sampleSize);
FloatAveragingBuffer avg1800 = new FloatAveragingBuffer(sampleSize);
IOscillator oscillator1200 = new Oscillator(1200.0, 7200.0);
IOscillator oscillator1800 = new Oscillator(1800.0, 7200.0);
oscillator1200.generateReal(1);
oscillator1800.generateReal(1);
float[] reference1200 = oscillator1200.generateReal(sampleSize);
float[] reference1800 = oscillator1800.generateReal(sampleSize);
boolean[] symbols = new boolean[]{true,false,true,false,true,false,true,false,true,true,true,false,true,false,
false,false,true,false,false,false,false,true,false,true,true,true,true,false,true,true,false,true,false,
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,
false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,true,false,
true,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,
false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false};
float[] samples = generateAFSK1200(symbols, 6, 0.5f);
mLog.debug("Length:" + samples.length);
int offset = 0;
float correlation1200 = 0.0f;
float correlation1800 = 0.0f;
while(offset + sampleSize < samples.length)
{
correlation1200 = 0.0f;
correlation1800 = 0.0f;
for(int x = 0; x < sampleSize; x++)
{
correlation1200 += reference1200[x] * samples[x + offset];
correlation1800 += reference1800[x] * samples[x + offset];
}
correlation1200 = avg1200.get(Math.abs(correlation1200));
correlation1800 = avg1800.get(Math.abs(correlation1800));
mLog.debug(offset + " 1200:" + format(correlation1200) + " 1800:" + format(correlation1800) +
" Decision: " + (correlation1200 > correlation1800 ? "0" : "1"));
offset++;
}
}
}

View File

@@ -0,0 +1,103 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
/**
* Zero crossing error detector. Provides symbol timing error correction values by detecting zero-crossings within
* each symbol period. Ideally there should be only 0 or 1 zero crossings within each symbol period. However, when
* there is a slight DC offset in the demodulated samples, there can be up to 2 zero crossings within a symbol
* period.
*
* This detector will only provide error corrections when there are 1 or 2 zero crossings detected. If the number
* of zero crossings is greater than 2, then it is assumed that there is no signal present and the returned timing
* error value is zero. This detector provides an error correction value relative to (approximately) the middle of
* a symbol period and therefore continuously attempts to align zero crossings to the middle of the period.
*
* Since this detector does NOT use interpolation, the error signal will contain minor levels of symbol timing
* jitter. Error signal values are reported as integrals of the sampling period.
*/
public class AFSKTimingErrorDetector
{
protected boolean[] mBuffer;
protected int mDetectedZeroCrossing;
protected float mSymbolMiddle;
private int mZeroCrossingCount;
/**
* Constructs the detector for the specified samples per symbol.
*
* @param samplesPerSymbol
*/
public AFSKTimingErrorDetector(int samplesPerSymbol)
{
mBuffer = new boolean[samplesPerSymbol];
mSymbolMiddle = (float)(samplesPerSymbol - 1) / 2.0f;
}
/**
* Stores the sample in the buffer. Use the getError() method once enough samples have been processed to determine
* the error for a symbol period.
*
* @param sample
*/
public void receive(boolean sample)
{
//Shift all samples left and store new sample at the end
System.arraycopy(mBuffer, 1, mBuffer, 0, mBuffer.length - 1);
mBuffer[mBuffer.length - 1] = sample;
}
/**
* Determines the symbol timing error for the samples processed thus far. This method should be invoked once per
* symbol period, triggered via an external samples per symbol tracking process.
*
* @return symbol timing error calculated as the distance between the approximate symbol center and the next closest
* zero crossing index. An error value of zero is returned if the number of zero crossings is less than one or
* greater than two.
*/
public int getError()
{
mZeroCrossingCount = 0;
for(int x = 0; x < mBuffer.length - 1; x++)
{
if(mBuffer[x] ^ mBuffer[x + 1])
{
mDetectedZeroCrossing = x + 1;
mZeroCrossingCount++;
if(mZeroCrossingCount > 1)
{
return 0; //Too many crossings
}
}
}
if(mZeroCrossingCount == 1)
{
if(mDetectedZeroCrossing > mSymbolMiddle)
{
return -(mBuffer.length - mDetectedZeroCrossing);
}
else
{
return mDetectedZeroCrossing + 1;
}
}
return 0;
}
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.afsk;
import javafx.beans.property.SimpleIntegerProperty;
public class AFSKTimingErrorDetectorInstrumented extends AFSKTimingErrorDetector
{
public SimpleIntegerProperty timingError = new SimpleIntegerProperty();
/**
* Constructs the detector for the specified samples per symbol.
*
* @param samplesPerSymbol
*/
public AFSKTimingErrorDetectorInstrumented(int samplesPerSymbol)
{
super(samplesPerSymbol);
}
@Override
public int getError()
{
int error = super.getError();
timingError.setValue(error);
return error;
}
public int getDetectedZeroCrossing()
{
return mDetectedZeroCrossing;
}
public boolean[] getBuffer()
{
return mBuffer;
}
}

View File

@@ -1,22 +1,82 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.am;
public abstract class AMDemodulator
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
/**
* Performs AM demodulation on baseband I/Q samples to produce demodulated float output.
*/
public class AMDemodulator
{
private float mGain;
public AMDemodulator( float gain )
{
mGain = gain;
}
public float demodulate( float inphase, float quadrature )
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("AMDemodulator");
private int mOutputBufferPointer;
private float mGain;
/**
* Constructs this demodulator where the specified gain is applied to demodulated output samples.
*
* @param gain to apply to demodulated output samples.
*/
public AMDemodulator(float gain)
{
return (float)Math.sqrt( ( inphase * inphase ) +
( quadrature * quadrature ) ) * mGain;
mGain = gain;
}
/**
* Sets the gain to the specified level.
*/
public void setGain(float gain)
{
mGain = gain;
}
/**
* Demodulates the comples I/Q sample and returns the demodulated output sample.
*
* @param inphase sample
* @param quadrature sample
* @return AM demodulated sample
*/
public float demodulate(float inphase, float quadrature)
{
return (float)Math.sqrt((inphase * inphase) + (quadrature * quadrature)) * mGain;
}
/**
* Demodulates the reusable comples sample buffer and returns a demodulated audio buffer.
* @param complexBuffer to demodulate
* @return demodulated audio buffer.
*/
public ReusableFloatBuffer demodulate(ReusableComplexBuffer complexBuffer)
{
ReusableFloatBuffer reusableFloatBuffer = mReusableBufferQueue.getBuffer(complexBuffer.getSampleCount());
float[] input = complexBuffer.getSamples();
float[] output = reusableFloatBuffer.getSamples();
mOutputBufferPointer = 0;
for(int x= 0; x < complexBuffer.getSamples().length; x += 2)
{
output[mOutputBufferPointer++] = demodulate(input[x], input[x + 1]);
}
complexBuffer.decrementUserCount();
return reusableFloatBuffer;
}
public void setGain( float gain )
{
mGain = gain;
}
}

View File

@@ -1,69 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This 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.am;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.real.RealBuffer;
public class AMDemodulator_CB extends AMDemodulator implements Listener<ComplexBuffer>
{
private Listener<RealBuffer> mListener;
public AMDemodulator_CB( float gain )
{
super( gain );
}
@Override
public void receive( ComplexBuffer buffer )
{
if( mListener != null )
{
float[] samples = buffer.getSamples();
int half = samples.length / 2;
float[] demodulated = new float[ half ];
for( int x = 0; x < half; x++ )
{
int index = x * 2;
demodulated[ x ] = demodulate( samples[ index ], samples[ index + 1 ] );
}
mListener.receive( new RealBuffer( demodulated ) );
}
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
public void dispose()
{
mListener = null;
}
}

View File

@@ -1,55 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.filter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
@Deprecated
public abstract class ComplexFilter implements Listener<Complex>
{
private Listener<Complex> mListener;
public void setListener( Listener<Complex> listener )
{
mListener = listener;
}
public void dispose()
{
mListener = null;
}
public Listener<Complex> getListener()
{
return mListener;
}
public boolean hasListener()
{
return mListener != null;
}
protected void send( Complex sample )
{
if( mListener != null )
{
mListener.receive( sample );
}
}
}

View File

@@ -22,10 +22,10 @@
******************************************************************************/
package io.github.dsheirer.dsp.filter;
import io.github.dsheirer.dsp.filter.cic.ComplexPrimeCICDecimate;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesignerWithLagrange;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,10 +34,14 @@ import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
/**
* Utility methods for designing various types of filters.
*/
public class FilterFactory
{
private final static Logger mLog =
LoggerFactory.getLogger(FilterFactory.class);
private final static Logger mLog = LoggerFactory.getLogger(FilterFactory.class);
private static final double PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE = -6.020599842071533; //decibel(0.5, 0.0)
private static final double MARGIN_OF_ERROR = 0.0003;
/**
* Generates coefficients for a unity-gain, windowed low-pass filter
@@ -48,43 +52,49 @@ public class FilterFactory
* @param window - to apply against the coefficients
* @return
*/
public static float[] getSinc(int sampleRate,
long frequency,
int length,
Window.WindowType window)
public static float[] getSinc(double sampleRate, long frequency, int length, Window.WindowType window)
{
//Ensure we have an odd length
assert (length % 2 == 0);
int evenLength = length % 2 == 0 ? length : length + 1;
//Get unity response array (one element longer to align with IDFT size)
float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, length + 1);
float[] frequencyResponse = getUnityResponseArray(sampleRate, frequency, evenLength);
//Apply Inverse DFT against frequency response unity values, leaving the
//IDFT bin results in the frequency response array
FloatFFT_1D idft = new FloatFFT_1D(length + 1);
idft.realInverseFull(frequencyResponse, false);
FloatFFT_1D idft = new FloatFFT_1D(evenLength);
idft.realInverseFull(frequencyResponse, true);
//Transfer the IDFT results to the return array
float[] coefficients = new float[length];
int middleCoefficient = (int) (length / 2);
int middleCoefficient = (int)(length / 2);
//Bin 0 of the idft is our center coefficient
coefficients[middleCoefficient] = frequencyResponse[0];
//The remaining idft bins from 1 to (middle - 1) are the mirror image
//coefficients
for(int x = 1; x < middleCoefficient; x++)
if(length % 2 == 0) //Even length
{
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
//The remaining idft bins from 1 to (middle - 1) are the mirror image
//coefficients
for(int x = 0; x < middleCoefficient; x++)
{
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
}
}
else
{
//Bin 0 of the idft is our center coefficient
coefficients[middleCoefficient] = frequencyResponse[0];
//The remaining idft bins from 1 to (middle - 1) are the mirror image
//coefficients
for(int x = 1; x < middleCoefficient; x++)
{
coefficients[middleCoefficient + x] = frequencyResponse[2 * x];
coefficients[middleCoefficient - x] = frequencyResponse[2 * x];
}
}
//Apply the window against the coefficients
coefficients = Window.apply(window, coefficients);
//Normalize to unity (1) gain
coefficients = normalize(coefficients);
return coefficients;
}
@@ -95,7 +105,7 @@ public class FilterFactory
* @param coefficients
* @return
*/
private static float[] normalize(float[] coefficients)
public static float[] normalize(float[] coefficients)
{
float accumulator = 0;
@@ -112,6 +122,32 @@ public class FilterFactory
return coefficients;
}
/**
* Normalizes all filter coefficients to achieve an overall objective gain, by ensuring
* that the sum of the absolute value of all coefficients adds up to the objective.
*
* @param coefficients
* @return
*/
public static float[] normalize(float[] coefficients, float objective)
{
float accumulator = 0;
for(int x = 0; x < coefficients.length; x++)
{
accumulator += Math.abs(coefficients[x]);
}
accumulator /= objective;
for(int x = 0; x < coefficients.length; x++)
{
coefficients[x] = coefficients[x] / accumulator;
}
return coefficients;
}
/**
* Constructs an array of unity (1) response values representing the
* desired (pre-windowing) frequency response, used by the sync function
@@ -128,17 +164,15 @@ public class FilterFactory
* @param length
* @return
*/
public static float[] getUnityResponseArray(int sampleRate,
long frequency,
int length)
public static float[] getUnityResponseArray(double sampleRate, long frequency, int length)
{
float[] unityArray = new float[length * 2];
int binCount = (int) ((Math.round(
(float) frequency / (float) sampleRate * (float) length)));
if(length % 2 == 0) //even length
{
int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length)));
for(int x = 0; x < binCount; x++)
{
unityArray[x] = 1.0f;
@@ -147,9 +181,11 @@ public class FilterFactory
}
else //odd length
{
int binCount = (int)((Math.round((float)frequency / (float)sampleRate * (float)length + 0.5)));
unityArray[0] = 1.0f;
for(int x = 1; x <= binCount; x++)
for(int x = 1; x < binCount; x++)
{
unityArray[x] = 1.0f;
unityArray[length - x] = 1.0f;
@@ -159,25 +195,6 @@ public class FilterFactory
return unityArray;
}
public static float[] getSine(float sampleRate, float frequency, int length)
{
float[] retVal = new float[length];
float radianFrequency = (float) (2.0d * Math.PI * (frequency / sampleRate));
int middle = (int) (length / 2);
for(int x = 0; x < middle; x++)
{
float val = (float) Math.sin(radianFrequency * x);
retVal[middle + x] = val;
retVal[middle - x] = -val;
}
return retVal;
}
/**
* Applies a repeating sequence of 1, -1 to the coefficients to invert
* the frequency response of the filter. Used in converting a low-pass
@@ -211,25 +228,9 @@ public class FilterFactory
* @param windowType - window to apply against the generated coefficients
* @return
*/
public static float[] getLowPass(int sampleRate,
long cutoff,
int filterLength,
Window.WindowType windowType)
public static float[] getLowPass(double sampleRate, long cutoff, int filterLength, Window.WindowType windowType)
{
if(filterLength % 2 == 0) //even length
{
float[] values = getSinc(sampleRate, cutoff, filterLength + 2, windowType);
//throw away the 0 index and the last index
return Arrays.copyOfRange(values, 1, values.length - 2);
}
else
{
float[] values = getSinc(sampleRate, cutoff, filterLength + 1, windowType);
//throw away the 0 index
return Arrays.copyOfRange(values, 1, values.length);
}
return getSinc(sampleRate, cutoff, filterLength, windowType);
}
/**
@@ -246,26 +247,17 @@ public class FilterFactory
* - passFrequency < stopFrequency
* - stopFrequency <= sampleRate/2
*/
public static float[] getLowPass(int sampleRate,
int passFrequency,
int stopFrequency,
int attenuation,
Window.WindowType windowType,
boolean forceOddLength)
public static float[] getLowPass(double sampleRate, int passFrequency, int stopFrequency, int attenuation,
Window.WindowType windowType, boolean forceOddLength)
{
if(stopFrequency < passFrequency || stopFrequency > (sampleRate / 2))
{
throw new IllegalArgumentException("FilterFactory - low pass "
+ "filter pass frequency [" + passFrequency + "] must be "
+ "less than the stop frequency [" + stopFrequency + "] "
+ "and must be less than half [" + (int) (sampleRate / 2) +
"] of the sample rate [" + sampleRate + "]");
throw new IllegalArgumentException("FilterFactory - low pass filter pass frequency [" + passFrequency +
"] must be less than the stop frequency [" + stopFrequency + "] and must be less than half [" +
(int)(sampleRate / 2) + "] of the sample rate [" + sampleRate + "]");
}
int tapCount = getTapCount(sampleRate,
passFrequency,
stopFrequency,
attenuation);
int tapCount = getTapCount(sampleRate, passFrequency, stopFrequency, attenuation);
if(forceOddLength)
{
@@ -289,34 +281,21 @@ public class FilterFactory
* @param windowType - window to apply against the generated coefficients
* @return
*/
public static float[] getHighPass(int sampleRate,
long cutoff,
int filterLength,
Window.WindowType windowType)
public static float[] getHighPass(int sampleRate, long cutoff, int filterLength, Window.WindowType windowType)
{
//Convert the high frequency cutoff to its low frequency cutoff
//equivalent, so that when we generate the low pass filter, prior to
//inversion, its at the correct frequency
long convertedCutoff = sampleRate / 2 - cutoff;
return invert(getSinc(sampleRate,
convertedCutoff,
filterLength,
windowType));
return invert(getSinc(sampleRate, convertedCutoff, filterLength, windowType));
}
public static float[] getHighPass(int sampleRate,
long stopFrequency,
long passFrequency,
int attenuation,
Window.WindowType windowType,
boolean forceOddLength)
public static float[] getHighPass(int sampleRate, long stopFrequency, long passFrequency, int attenuation,
Window.WindowType windowType, boolean forceOddLength)
{
/* reverse the stop and pass frequency to get the low pass variant */
int tapCount = getTapCount(sampleRate,
stopFrequency,
passFrequency,
attenuation);
int tapCount = getTapCount(sampleRate, stopFrequency, passFrequency, attenuation);
if(forceOddLength)
{
@@ -360,11 +339,10 @@ public class FilterFactory
}
/**
* Determines the number of fir filter taps required to produce the
* specified frequency response with passband ripple near .1dB.
* Determines the number of fir filter taps required to produce the specified frequency response with passband
* ripple near 0.1dB.
*
* Implements the algorithm from Understanding Digital Signal Processing, 3e
* , Lyons, section 5.10.5.
* Implements the algorithm from Understanding Digital Signal Processing, 3e, Lyons, section 5.10.5.
*
* @param sampleRate in hertz
* @param pass pass frequency in hertz
@@ -372,24 +350,11 @@ public class FilterFactory
* @param attenuation in dB
* @return
*/
public static int getTapCount(int sampleRate,
long pass,
long stop,
int attenuation)
public static int getTapCount(double sampleRate, long pass, long stop, int attenuation)
{
double frequency = ((double) stop - (double) pass) / (double) sampleRate;
double frequency = ((double)stop - (double)pass) / sampleRate;
return (int) (Math.round((double) attenuation / (22.0d * frequency)));
}
public static ComplexPrimeCICDecimate getDecimationFilter(int sampleRate,
int decimatedRate, int order, int passFrequency, int attenuation,
Window.WindowType windowType)
{
int decimationRate = (int) (sampleRate / decimatedRate);
return new ComplexPrimeCICDecimate(decimationRate, order,
passFrequency, attenuation, windowType);
return (int)(Math.round((double)attenuation / (22.0d * frequency)));
}
/**
@@ -401,10 +366,7 @@ public class FilterFactory
* polyphase filter decimation chain
* @throws - AssertionException if sample rate is not a multiple of 48 kHz
*/
public static int[] getPolyphaseDecimationRates(int sampleRate,
int decimatedRate,
long passFrequency,
long stopFrequency)
public static int[] getPolyphaseDecimationRates(int sampleRate, int decimatedRate, long passFrequency, long stopFrequency)
{
int[] rates;
@@ -414,7 +376,7 @@ public class FilterFactory
+ "integer multiple of sample rate");
}
int decimation = (int) (sampleRate / decimatedRate);
int decimation = (int)(sampleRate / decimatedRate);
//Decimation rates below 20 will use single stage polyphase filter
if(decimation < 20)
@@ -432,11 +394,6 @@ public class FilterFactory
int stage1 = findClosest(optimalStage1, factors);
// mLog.info( "Decimation rate [" + decimation +
// "] stage1 optimal [" + optimalStage1 +
// "] stage1 actual [" + stage1 +
// "]");
if(stage1 == decimation || stage1 == 1)
{
rates = new int[1];
@@ -447,7 +404,7 @@ public class FilterFactory
{
rates = new int[2];
int stage2 = (int) (decimation / stage1);
int stage2 = (int)(decimation / stage1);
if(stage1 > stage2)
{
@@ -465,8 +422,7 @@ public class FilterFactory
}
/**
* Finds the factor that is closest to the desired factor, from an ordered
* list of factors.
* Finds the factor that is closest to the desired factor, from an ordered list of factors.
*/
private static int findClosest(int desiredFactor, Set<Integer> factors)
{
@@ -488,9 +444,8 @@ public class FilterFactory
}
/**
* Determines the factors that make up an integer. Uses a brute force
* method to iterate all integers from 1 to value, determining which factors
* are evenly divisible into the value.
* Determines the factors that make up an integer. Uses a brute force method to iterate all integers from 1 to
* value, determining which factors are evenly divisible into the value.
*
* @param value - integer decimation value
* @return - ordered set of factors for value
@@ -498,11 +453,11 @@ public class FilterFactory
private static Set<Integer> getFactors(int value)
{
Set<Integer> factors = new TreeSet<Integer>();
/* Brute force */
/* Brute force */
for(int x = 1; x <= value; x++)
{
int remainder = (int) (value / x);
int remainder = (int)(value / x);
if(remainder * x == value)
{
@@ -514,14 +469,10 @@ public class FilterFactory
}
/**
* Determines the optimal decimation rate for stage 1 of a two-stage
* poly-phase decimation filter chain, to produce a final sample rate of
* 48 kHz using a pass bandwidth of 25 kHz.
* Determines the optimal decimation rate for stage 1 of a two-stage poly-phase decimation filter chain, to produce
* a final sample rate of 48 kHz using a pass bandwidth of 25 kHz. Use for total decimation rates of 20 or higher.
*
* Use for total decimation rates of 20 or higher.
*
* Implements the algorithm described in Lyons, Understanding Digital Signal
* Processing, 3e, section 10.2.1, page 511.
* Implements the algorithm described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511.
*
* @param sampleRate
* @param decimation
@@ -529,42 +480,29 @@ public class FilterFactory
* @return optimum integer decimation rate for the first stage decimation
* filter
*/
public static int getOptimalStageOneRate(int sampleRate,
int decimation,
long passFrequency,
long stopFrequency)
public static int getOptimalStageOneRate(int sampleRate, int decimation, long passFrequency, long stopFrequency)
{
double ratio = getBandwidthRatio(passFrequency, stopFrequency);
double numerator = 1.0d - (
Math.sqrt((double) decimation * ratio / (2.0d - ratio)));
double numerator = 1.0d - (Math.sqrt((double)decimation * ratio / (2.0d - ratio)));
double denominator = 2.0d - (ratio * (decimation + 1.0d));
int retVal = (int) (2.0d * decimation * (numerator / denominator));
// mLog.info( "Optimal Stage 1 Decimation - rate [" + sampleRate +
// "] pass [" + passFrequency +
// "] bw ratio [" + ratio +
// "] optimal [" + retVal +
// "]" );
int retVal = (int)(2.0d * decimation * (numerator / denominator));
return retVal;
}
/**
* Determines the F ratio as described in Lyons, Understanding Digital
* Signal Processing, 3e, section 10.2.1, page 511
*
* Used in conjunction with the optimal stage one decimation rate method
* above.
* Determines the F ratio as described in Lyons, Understanding Digital Signal Processing, 3e, section 10.2.1, page 511
* Used in conjunction with the optimal stage one decimation rate method above.
*/
private static double getBandwidthRatio(long passFrequency, long stopFrequency)
{
assert (passFrequency < stopFrequency);
return ((double) (stopFrequency - passFrequency) /
(double) stopFrequency);
return ((double)(stopFrequency - passFrequency) /
(double)stopFrequency);
}
/**
@@ -575,15 +513,12 @@ public class FilterFactory
* @param outputSampleRate
* @return
*/
public static float[] getCICCleanupFilter(int outputSampleRate,
int passFrequency,
int attenuation,
Window.WindowType window)
public static float[] getCICCleanupFilter(int outputSampleRate, int passFrequency, int attenuation, Window.WindowType window)
{
int taps = getTapCount(outputSampleRate, passFrequency, passFrequency + 1500,
attenuation);
/* Make tap count odd */
/* Make tap count odd */
if(taps % 2 == 0)
{
taps++;
@@ -599,7 +534,7 @@ public class FilterFactory
//Transfer the IDFT results to the odd length return array
float[] coefficients = new float[taps];
int middleCoefficient = (int) (taps / 2);
int middleCoefficient = (int)(taps / 2);
//Bin 0 of the idft is our center coefficient
coefficients[middleCoefficient] = frequencyResponse[0];
@@ -620,26 +555,22 @@ public class FilterFactory
return coefficients;
}
public static float[] getCICResponseArray(int sampleRate,
int frequency,
int length)
public static float[] getCICResponseArray(int sampleRate, int frequency, int length)
{
float[] cicArray = new float[length * 2];
int binCount = (int) ((Math.round(
(double) frequency / (double) sampleRate * 2.0d * (double) length)));
int binCount = (int)((Math.round(
(double)frequency / (double)sampleRate * 2.0d * (double)length)));
cicArray[0] = 1.0f;
float unityResponse = (float) (Math.sin(1.0d / (double) length) /
(1.0d / (double) length));
float unityResponse = (float)(Math.sin(1.0d / (double)length) /
(1.0d / (double)length));
for(int x = 1; x <= binCount; x++)
{
/* Calculate unity response plus amplification for droop */
float compensated = 1.0f + (unityResponse -
(float) (Math.sin((double) x / (double) length) /
((double) x / (double) length)));
/* Calculate unity response plus amplification for droop */
float compensated = 1.0f + (unityResponse - (float)(Math.sin((double)x / (double)length) / ((double)x / (double)length)));
cicArray[x] = compensated;
cicArray[length - x] = compensated;
@@ -649,28 +580,29 @@ public class FilterFactory
}
/**
* Creates root raised cosine filter coefficients with a tap count equal
* to the symbols x samplesPerSymbol + 1.
* Creates root raised cosine filter coefficients with a tap count equal to the symbols x samplesPerSymbol + 1.
*
* Symbol count should be an even integer.
*
* Ported to java from gnuradio/filter/firdes.cc
*
* For 40db attenuation, calculate the number of symbols based on the
* following formula:
*
* For 40db attenuation, calculate the number of symbols based on the following formula:
* Symbols = -44 * alpha + 33
*
* Polyphase Channelizer notes:
* -Set samples Per Symbol at 2 (or more) * number of channels
* -Set symbolCount to sufficient size to produce required attenuation
* -Set symbol rate in hertz
* -Set alpha = (desired channel bandwidth / (symbol rate * samples per symbol) - 1.0
* -Channel bandwidth must be larger than symbol rate * samples per symbol, in order for there to be a positive alpha
*
* @param samplesPerSymbol - number of samples per symbol
* @param symbols - number of symbols - must be even
* @param alpha - roll-off factor
* @param symbolCount - number of symbol intervals for the filter. This directly impacts the filter size
* @param alpha - roll-off factor.
* @return - filter coefficients
*/
public static float[] getRootRaisedCosine(float samplesPerSymbol,
int symbols,
float alpha)
public static float[] getRootRaisedCosine(int samplesPerSymbol, int symbolCount, float alpha)
{
int taps = (int)(samplesPerSymbol * (float)symbols + 1.0f);
int taps = samplesPerSymbol * symbolCount;
float scale = 0;
@@ -678,10 +610,10 @@ public class FilterFactory
for(int x = 0; x < taps; x++)
{
float index = (float) x - ((float) taps / 2.0f);
float index = (float)x - ((float)taps / 2.0f);
float x1 = (float) Math.PI * index / samplesPerSymbol;
float x2 = 4.0f * alpha * index / samplesPerSymbol;
float x1 = (float)Math.PI * index / (float)samplesPerSymbol;
float x2 = 4.0f * alpha * index / (float)samplesPerSymbol;
float x3 = x2 * x2 - 1.0f;
float numerator, denominator;
@@ -690,17 +622,15 @@ public class FilterFactory
{
if(x != taps / 2)
{
numerator = (float) Math.cos((1.0 + alpha) * x1) +
(float) Math.sin((1.0f - alpha) * x1) /
(4.0f * alpha * index / samplesPerSymbol);
numerator = (float)Math.cos((1.0 + alpha) * x1) +
(float)Math.sin((1.0f - alpha) * x1) / (4.0f * alpha * index / (float)samplesPerSymbol);
}
else
{
numerator = (float) Math.cos((1.0f + alpha) * x1) +
(1.0f - alpha) * (float) Math.PI / (4.0f * alpha);
numerator = (float)Math.cos((1.0f + alpha) * x1) + (1.0f - alpha) * (float)Math.PI / (4.0f * alpha);
}
denominator = x3 * (float) Math.PI;
denominator = x3 * (float)Math.PI;
}
else
{
@@ -713,14 +643,11 @@ public class FilterFactory
x3 = (1.0f - alpha) * x1;
x2 = (1.0f + alpha) * x1;
numerator = (float) (Math.sin(x2) * (1.0 + alpha) * Math.PI -
Math.cos(x3) * ((1.0 - alpha) * Math.PI *
(double) samplesPerSymbol) / (4.0 * alpha * index) +
Math.sin(x3) * (double) samplesPerSymbol *
(double) samplesPerSymbol / (4.0 * alpha * index * index));
numerator = (float)(Math.sin(x2) * (1.0f + alpha) * Math.PI - Math.cos(x3) * ((1.0 - alpha) * Math.PI *
(double)samplesPerSymbol) / (4.0 * alpha * index) + Math.sin(x3) * (double)samplesPerSymbol *
(double)samplesPerSymbol / (4.0 * alpha * index * index));
denominator = (float) (-32.0 * Math.PI * alpha * alpha * index /
(double) samplesPerSymbol);
denominator = (float)(-32.0 * Math.PI * alpha * alpha * index / (double)samplesPerSymbol);
}
coefficients[x] = 4.0f * alpha * numerator / denominator;
@@ -740,10 +667,10 @@ public class FilterFactory
}
/**
* Creates a filter from the filter specification
* Creates a filter from the filter specification using the remez exchange design algorithm
*
* @param specification
* @return
* @return filter coefficients
* @throws FilterDesignException if the filter cannot be designed
*/
public static float[] getTaps(FIRFilterSpecification specification) throws FilterDesignException
@@ -757,4 +684,349 @@ public class FilterFactory
return null;
}
/**
* Evaluates the filter to determine the magnitude response (dB) at the specified frequency.
*
* @param filter to evaluate
* @param frequency to calculate (0 <> 0.5)
* @return magnitude frequency response in decibels
*/
public static double evaluate(float[] filter, double frequency)
{
double real = 0.0;
double imag = 0.0;
for(int x = 0; x < filter.length; x++)
{
real += filter[x] * Math.cos(Math.PI * frequency * (double)x);
imag += filter[x] * Math.sin(Math.PI * frequency * (double)x);
}
return decibel(real, imag);
}
/**
* Calculates the decibel magnitude of the real and imaginary complex sample
*
* @param real (x-axis) value
* @param imag (y-axis) value
* @return magnitude in decibels
*/
public static double decibel(double real, double imag)
{
return (float)(10.0 * Math.log10(Math.pow(real, 2.0) + Math.pow(imag, 2.0)));
}
public static float[] getRemezChannelizer(int channelBandwidth, int channels, int tapsPerChannel, double alpha,
double passRipple, double stopRipple) throws FilterDesignException
{
FIRFilterSpecification specification = FIRFilterSpecification.channelizerBuilder()
.sampleRate(channels * channelBandwidth * 4)
.channels(channels)
.channelBandwidth(channelBandwidth)
.tapsPerChannel(tapsPerChannel)
.alpha(alpha)
.passRipple(passRipple)
.stopRipple(stopRipple)
.build();
RemezFIRFilterDesignerWithLagrange designer = new RemezFIRFilterDesignerWithLagrange(specification);
float[] taps = designer.getImpulseResponse();
double bandEdgeFrequency = (double)channelBandwidth / (double)(channels * channelBandwidth * 2);
double response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI)), 0.0);
mLog.debug("Frequency Response at 1.0: " + response);
response = decibel(designer.getFrequencyResponse(Math.cos(bandEdgeFrequency * Math.PI * 2.0)), 0.0);
mLog.debug("Frequency Response at 2.0: " + response);
return taps;
}
/**
* Polyphase M2 synthesizer sync filter. Designed for multiple M2 oversampled channel inputs and
* an M*channel count output.
*
* @param channelSampleRate per input channel
* @param channelBandwidth per input channel
* @param channels input count
* @param tapsPerChannel minumum. This may be increased to meet the band edge -6.02dB requirement
* @return filter
* @throws FilterDesignException
*/
public static float[] getSincM2Synthesizer(double channelSampleRate, double channelBandwidth, int channels,
int tapsPerChannel) throws FilterDesignException
{
int filterLength = (channels * tapsPerChannel) - 1;
double cutoff = (channelBandwidth * 1.10) / (channelSampleRate * (double)channels);
//Design the prototype synthesizer with 105% of the channel bandwidth produced by the channelizer.
float[] taps = FilterFactory.getKaiserSinc(filterLength, cutoff, 80.0);
//This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient
float[] extendedTaps = new float[taps.length + 1];
System.arraycopy(taps, 0, extendedTaps, 1, taps.length);
return extendedTaps;
}
/**
* Creates a windowed-sync (Nyquist) M/2 prototype FIR filter for use with a Polyphase Channelizer/Synthesizer.
* The filter is designed for x2 oversampling of each channel and the filter cutoff frequency is incrementally
* adjusted to achieve a -6.02 dB attenuation at the channel band edge to enable perfect reconstruction of adjacent
* channels.
*
* The odd length sync filter that meets the design objectives will be increased in length by pre-padding a zero
* coefficient to produce a filter with a length that is an integral of the channel count.
*
* Note: the tapsPerChannel value will be iteratively increased (up to 10x) if a filter cannot be designed to meet
* the channel band edge attenuation objective.
*
* Note: a channelizer filter designed using a Blackman-Harris-7 window and 18-21 taps per channel creates a filter
* with attenuation in range of -60 to -80 dB at the edge of the 2x sampled channel.
*
* @param channelBandwidth - full bandwidth of the channel in hertz
* @param channels - number of channels
* @param tapsPerChannel - desired filter tap count per channel (may be increased to meet objective)
* @param windowType - to use when designing the windowed-sync filter
* @param logResults - to log a debug summary of the filter characteristics
*
* @return filter coefficients
*
* @throws FilterDesignException if a filter cannot be designed with a band edge attenuation of -6.02 dB
*/
/**
* Polyphase M2 channelizer sync filter. Designed for an M*channel input and an M2 channel output.
*
* @param channelBandwidth per channel
* @param channels count
* @param tapsPerChannel minimum. This may be increased to meet the band edge -6.02dB requirement
* @param logResults to log the results of the design
* @return filter
* @throws FilterDesignException if the filter cannot be designed with a band edge of -6.02dB
*/
public static float[] getSincM2Channelizer(double channelBandwidth, int channels, int tapsPerChannel,
boolean logResults) throws FilterDesignException
{
int currentTapsPerChannel = tapsPerChannel;
int filterLength = (channels * currentTapsPerChannel) - 1;
double sampleRate = channelBandwidth * channels;
double bandEdge = channelBandwidth / sampleRate;
double cutoffFrequency = bandEdge / 2.0;
double increment = cutoffFrequency * 0.1;
//Get an initial filter and band edge frequency response using the channel bandwidth as a cutoff
float[] taps = null;
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
double response = FilterFactory.evaluate(taps, bandEdge);
//Set cutoff adjustment threshold - we'll test cutoff frequencies to around 1 hertz resolution
double incrementThreshold = 1.0 / sampleRate;
//Iteratively evaluate filters to find the filter with the highest cutoff frequency that meets the objective
while(increment > incrementThreshold)
{
//If the current cutoff meets the objective, test if a higher cutoff also meets the objective
if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR) &&
(cutoffFrequency + increment <= bandEdge))
{
float[] higherCutoffTaps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency + increment, 80.0);
double higherCutoffResponse = FilterFactory.evaluate(higherCutoffTaps, bandEdge);
if(matchesObjective(higherCutoffResponse, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
{
cutoffFrequency += increment;
taps = higherCutoffTaps;
response = higherCutoffResponse;
}
else
{
increment /= 2.0;
}
}
//If the current cutoff meets the objective, decrease the increment to test for smaller resolution cutoffs
else if(matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
{
increment /= 2.0;
}
//Decrease the cutoff frequency - we can't meet the objective
else
{
cutoffFrequency -= increment;
if(cutoffFrequency <= 0)
{
if(logResults)
{
mLog.debug("Warning: cannot design channelizer filter with tap count [" + currentTapsPerChannel +
"] increasing tap count and starting over");
}
currentTapsPerChannel++;
//After we've attempted increasing the taps per channel 10x, give up
if(currentTapsPerChannel > (tapsPerChannel + 10))
{
throw new FilterDesignException("Couldn't design filter with taps per channel count in the " +
"range of " + tapsPerChannel + " - " + (tapsPerChannel + 10) + " Sample Rate:" + sampleRate + " Channels:" + channels);
}
filterLength = channels * currentTapsPerChannel - 1;
cutoffFrequency = channelBandwidth / sampleRate;
increment = cutoffFrequency * 0.1;
}
taps = FilterFactory.getKaiserSinc(filterLength, cutoffFrequency, 80.0);
response = FilterFactory.evaluate(taps, bandEdge);
}
}
//This will probably never happen, but ...
if(!matchesObjective(response, PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE, MARGIN_OF_ERROR))
{
throw new FilterDesignException("Cannot design filter to specifications");
}
//This is an odd length filter - increase the length by 1 by pre-padding a zero coefficient
float[] extendedTaps = new float[taps.length + 1];
System.arraycopy(taps, 0, extendedTaps, 1, taps.length);
if(logResults)
{
mLog.debug("Polyphase Channelizer Filter Design Summary");
mLog.debug("-----------------------------------------------------");
mLog.debug("Input Sample Rate: " + sampleRate);
mLog.debug("Channel Bandwidth: " + channelBandwidth);
mLog.debug("Channels: " + channels);
mLog.debug("Window Type: " + Window.WindowType.KAISER.name());
mLog.debug("Taps Per Channel - Requested:" + tapsPerChannel + " Actual:" + ((double)extendedTaps.length / (double)channels));
mLog.debug("Filter Length: " + (extendedTaps.length));
mLog.debug("Requested Cutoff Frequency: " + (sampleRate * bandEdge));
mLog.debug("Actual Cutoff Frequency: " + (sampleRate * cutoffFrequency));
mLog.debug("Attenuation at 0.25 Channels: " + evaluate(taps, bandEdge * 0.25) + "\tFrequency: " + (sampleRate * bandEdge * 0.25) + " [" + (bandEdge * 0.25) + "]");
mLog.debug("Attenuation at 0.50 Channels: " + evaluate(taps, bandEdge * 0.50) + "\tFrequency: " + (sampleRate * bandEdge * 0.50) + " [" + (bandEdge * 0.50) + "]");
mLog.debug("Attenuation at 0.75 Channels: " + evaluate(taps, bandEdge * 0.75) + "\tFrequency: " + (sampleRate * bandEdge * 0.75) + " [" + (bandEdge * 0.75) + "]");
mLog.debug("Attenuation OBJECTIVE: " + PERFECT_RECONSTRUCTION_GAIN_AT_BAND_EDGE);
mLog.debug("Attenuation at 1.00 Channels: " + evaluate(taps, bandEdge * 1.00) + "\tFrequency: " + (sampleRate * bandEdge * 1.0) + " [" + (bandEdge * 1.0) + "]");
mLog.debug("Attenuation at 1.25 Channels: " + evaluate(taps, bandEdge * 1.25) + "\tFrequency: " + (sampleRate * bandEdge * 1.25) + " [" + (bandEdge * 1.25) + "]");
mLog.debug("Attenuation at 1.50 Channels: " + evaluate(taps, bandEdge * 1.50) + "\tFrequency: " + (sampleRate * bandEdge * 1.5) + " [" + (bandEdge * 1.5) + "]");
mLog.debug("Attenuation at 1.75 Channels: " + evaluate(taps, bandEdge * 1.75) + "\tFrequency: " + (sampleRate * bandEdge * 1.75) + " [" + (bandEdge * 1.75) + "]");
mLog.debug("Attenuation at 2.00 Channels: " + evaluate(taps, bandEdge * 2.00) + "\tFrequency: " + (sampleRate * bandEdge * 2.0) + " [" + (bandEdge * 2.0) + "]");
}
return extendedTaps;
}
/**
* Creates a windowed-sync (Nyquist) filter.
*
* @param cutoff frequency (0.0 <> 0.5)
* @param length of the filter - must be odd-length
* @param windowType to use in designing the filter
* @return filter coefficients.
* @throws FilterDesignException if the requested length is not odd
*/
public static float[] getSinc(double cutoff, int length, Window.WindowType windowType) throws FilterDesignException
{
if(length % 2 == 0)
{
throw new FilterDesignException("Sinc filters must be odd-length");
}
float[] coefficients = new float[length];
int half = length / 2;
double[] window = Window.getWindow(windowType, length);
double scalor = 2.0 * cutoff;
double piScalor = Math.PI * scalor;
coefficients[half] = (float)(1.0 * scalor * window[half]);
for(int x = 1; x <= half; x++)
{
double a = piScalor * x;
double coefficient = scalor * Math.sin(a) / a;
coefficient *= window[half + x];
coefficients[half + x] = (float)coefficient;
coefficients[half - x] = (float)coefficient;
}
return coefficients;
}
/**
* Creates a Nyquist filter using a Kaiser Window.
*
* @param cutoff frequency (0.0 <> 0.5)
* @param length of the filter - must be odd-length
* @param attenuation desired for adjacent channels
* @return filter coefficients.
* @throws FilterDesignException if the requested length is not odd
*/
public static float[] getKaiserSinc(int length, double cutoff, double attenuation) throws FilterDesignException
{
if(length % 2 == 0)
{
throw new FilterDesignException("Sinc filters must be odd-length");
}
float[] coefficients = new float[length];
int half = length / 2;
double[] window = Window.getKaiser(length, attenuation);
double scalor = 2.0 * cutoff;
double piScalor = Math.PI * scalor;
coefficients[half] = (float)(1.0 * scalor * window[half]);
for(int x = 1; x <= half; x++)
{
double a = piScalor * x;
double coefficient = scalor * Math.sin(a) / a;
coefficient *= window[half + x];
coefficients[half + x] = (float)coefficient;
coefficients[half - x] = (float)coefficient;
}
return coefficients;
}
/**
* Compares two doubles for equals and avoids any rounding error that are present.
*
* @param a value to compare
* @param objective value to compare
* @return true if they are equivalent within the margin of error
*/
private static boolean matchesObjective(double a, double objective, double marginOfError)
{
return Math.abs(a - objective) <= marginOfError;
}
public static void main(String[] args)
{
float[] taps = null;
try
{
taps = FilterFactory.getSincM2Channelizer(12500.0, 800, 9, true);
}
catch(FilterDesignException fde)
{
mLog.error("Error", fde);
}
mLog.debug("Done: " + Arrays.toString(taps));
}
}

View File

@@ -1719,7 +1719,32 @@ public enum Filters
0.011873357051047719f, 0.0f, -0.007901319195893647f, 0.0f,
0.005055504379767936f, 0.0f, -0.003054430179754289f, 0.0f,
0.001695637278417295f, 0.0f, -0.000998606272947510f
} );
}),
/**
* FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
* .sampleRate(100)
* .gridDensity(16)
* .oddLength(true)
* .passBandCutoff(21)
* .passBandAmplitude(1.0)
* .passBandRipple(0.01)
* .stopBandStart(29)
* .stopBandAmplitude(0.0)
* .stopBandRipple(0.01) //Approximately -85 dB suppression
* .build();
*/
HALF_BAND_FILTER_27T(new float[]
{
0.007495115f, 0.0f, -0.011045938f, 0.0f,
0.019129561f, 0.0f, -0.031747308f, 0.0f,
0.053220540f, 0.0f, -0.099569574f, 0.0f,
0.316083550f, 0.5f, 0.316083550f, 0.0f,
-0.099569574f, 0.0f, 0.053220540f, 0.0f,
-0.031747308f, 0.0f, 0.019129561f, 0.0f,
-0.011045938f, 0.0f, 0.007495115f
});
float[] mCoefficients;

View File

@@ -1,234 +1,523 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
/*
* Kaiser Window design methods: getKaiser(), getKaiserBeta() and getBesselZerothOrder() with minor naming changes were
* adopted from:
*
* https://github.com/rjeschke/neetutils-base/blob/master/src/main/java/com/github/rjeschke/neetutils/audio/FIRUtils.java
*
* Copyright (C) 2012 René Jeschke <rene_jeschke@yahoo.de>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.github.dsheirer.dsp.filter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
/**
* Window creator
*
* Blackman, Hamming and Hanning windows
* based on windowing methods described at:
* http://www.mstarlabs.com/dsp/goertzel/goertzel.html
* and
* Understanding Digital Signal Processing 3e, Lyons, p.91
* and
* Digital Filters for Everyone, Allred, p. 132
*
* Window factory and corresponding utility methods.
*/
public class Window
{
public static double[] getWindow( WindowType type, int length )
private final static Logger mLog = LoggerFactory.getLogger(Window.class);
private static final double TWO_PI = Math.PI * 2.0d;
private static final double FOUR_PI = Math.PI * 4.0d;
private static final double SIX_PI = Math.PI * 6.0d;
private static final double EIGHT_PI = Math.PI * 8.0d;
/**
* Creates a window of the specified type and length.
*
* @param type of window
* @param length of window
* @return window of the specified type and length
*/
public static double[] getWindow(WindowType type, int length)
{
switch( type )
{
case BLACKMAN:
return getBlackmanWindow( length );
case COSINE:
return getCosineWindow( length );
case HAMMING:
return getHammingWindow( length );
case HANNING:
return getHanningWindow( length );
case NONE:
default:
return getRectangularWindow( length );
}
switch(type)
{
case BLACKMAN:
return getBlackman(length);
case BLACKMAN_HARRIS_4:
return getBlackmanHarris4(length);
case BLACKMAN_HARRIS_7:
return getBlackmanHarris7(length);
case BLACKMAN_NUTALL:
return getBlackmanNutall(length);
case COSINE:
return getCosine(length);
case FLAT_TOP:
return getFlatTop(length);
case HAMMING:
return getHamming(length);
case HANN:
return getHann(length);
case KAISER:
throw new IllegalArgumentException("Kaiser Window cannot be created via this method. Use the getKaiser() method instead.");
case NUTALL:
return getNutall(length);
case NONE:
default:
return getRectangular(length);
}
}
public static double[] getRectangularWindow( int length )
/**
* Creates an all-pass or rectangular window.
*
* @param length of the window
* @return rectangular window
*/
public static double[] getRectangular(int length)
{
double[] coefficients = new double[ length ];
double[] coefficients = new double[length];
Arrays.fill( coefficients, 1.0d );
return coefficients;
}
public static double[] getCosineWindow( int length )
{
double[] coefficients = new double[ length ];
Arrays.fill(coefficients, 1.0d);
if( length % 2 == 0 ) //Even length
{
int half = (int)( ( length - 1 ) / 2 );
for( int x = -half; x < length / 2 + 1; x++ )
{
coefficients[ x + half ] = Math.cos(
( (double)x * Math.PI ) / ( (double)length + 1.0d ) );
}
}
else //Odd length
{
int half = (int) length / 2;
for( int x = -half; x < half + 1; x++ )
{
coefficients[ x + half ] = Math.cos(
( (double)x * Math.PI ) / ( (double)length + 1.0d ) );
}
}
return coefficients;
return coefficients;
}
public static double[] getBlackmanWindow( int length )
/**
* Creates a cosine window of the specified length.
*
* @param length of the window
* @return cosine window
*/
public static double[] getCosine(int length)
{
double[] coefficients = new double[ length ];
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .426591D -
( .496561D * Math.cos( ( Math.PI * 2.0D * (double)x ) / (double)( length - 1 ) ) +
( .076848D * Math.cos( ( Math.PI * 4.0D * (double)x ) / (double)( length - 1 ) ) ) );
}
double[] coefficients = new double[length];
return coefficients;
}
public static double[] getHammingWindow( int length )
{
double[] coefficients = new double[ length ];
if( length % 2 == 0 ) //Even length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .54D - ( .46D *
Math.cos( ( Math.PI * ( 2.0D * (double)x + 1.0d ) ) / (double)( length - 1 ) ) );
}
}
else //Odd length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .54D - ( .46D *
Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) );
}
}
if(length % 2 == 0) //Even length
{
int half = (int)((length - 1) / 2);
return coefficients;
for(int x = -half; x < length / 2 + 1; x++)
{
coefficients[x + half] = Math.cos(
((double)x * Math.PI) / ((double)length + 1.0d));
}
}
else //Odd length
{
int half = (int)length / 2;
for(int x = -half; x < half + 1; x++)
{
coefficients[x + half] = Math.cos(
((double)x * Math.PI) / ((double)length + 1.0d));
}
}
return coefficients;
}
public static double[] getHanningWindow( int length )
/**
* Creates a 3-term Blackman window.
*
* @param length of the window
* @return Blackman window
*/
public static double[] getBlackman(int length)
{
double[] coefficients = new double[ length ];
double[] coefficients = new double[length];
if( length % 2 == 0 ) //Even length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .5D - ( .5D *
Math.cos( ( Math.PI * ( 2.0D * (double)x + 1 ) ) / (double)( length - 1 ) ) );
}
}
else //Odd length
{
for( int x = 0; x < length; x++ )
{
coefficients[ x ] = .5D - ( .5D *
Math.cos( ( 2.0D * Math.PI * (double)x ) / (double)( length - 1 ) ) );
}
}
double denominator = length - 1;
double a0 = 0.426590713672;
double a1 = 0.496560619089;
double a2 = 0.0768486672399;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos((TWO_PI * (double)x) / denominator)) +
(a2 * Math.cos((FOUR_PI * (double)x) / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Nutall window
*
* @param length of the window
* @return Nutall window
*/
public static double[] getNutall(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.355768;
double a1 = 0.487396;
double a2 = 0.144232;
double a3 = 0.012604;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Blackman-Nutall window
*
* @param length of the window
* @return Blackman-Nutall window
*/
public static double[] getBlackmanNutall(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.3635819;
double a1 = 0.4891775;
double a2 = 0.1365995;
double a3 = 0.0106411;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Blackman-Harris window
*
* @param length of the window
* @return Blackman-Harris window
*/
public static double[] getBlackmanHarris4(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.35875;
double a1 = 0.48829;
double a2 = 0.14128;
double a3 = 0.01168;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 7-term Blackman-Harris window
*
* @param length of the window
* @return Blackman-Harris window
*/
public static double[] getBlackmanHarris7(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.27105140069342;
double a1 = 0.43329793923448;
double a2 = 0.21812299954311;
double a3 = 0.06592544638803;
double a4 = 0.01081174209837;
double a5 = 0.00077658482522;
double a6 = 0.00001388721735;
return coefficients;
for(int x = 0; x < length; x++)
{
double w = TWO_PI * (double)x / denominator;
coefficients[x] = a0 -
(a1 * Math.cos(w)) +
(a2 * Math.cos(2.0 * w)) -
(a3 * Math.cos(3.0 * w)) +
(a4 * Math.cos(4.0 * w)) -
(a5 * Math.cos(5.0 * w)) +
(a6 * Math.cos(6.0 * w));
}
return coefficients;
}
/**
* Creates a Hamming window
*
* @param length of the window
* @return Hamming window
*/
public static double[] getHamming(int length)
{
double[] coefficients = new double[length];
double a0 = 0.54;
double a1 = 0.46;
double denominator = length;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a Hann window
*
* @param length of the window
* @return Hann window
*/
public static double[] getHann(int length)
{
double[] coefficients = new double[length];
double a0 = 0.5;
double a1 = 0.5;
double denominator = length;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 - (a1 * Math.cos(TWO_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Creates a 4-term Blackman-Harris window
*
* @param length of the window
* @return Blackman-Harris window
*/
public static double[] getFlatTop(int length)
{
double[] coefficients = new double[length];
double denominator = length - 1;
double a0 = 0.215578948;
double a1 = 0.41663158;
double a2 = 0.277263158;
double a3 = 0.083578947;
double a4 = 0.006947368;
for(int x = 0; x < length; x++)
{
coefficients[x] = a0 -
(a1 * Math.cos(TWO_PI * (double)x / denominator)) +
(a2 * Math.cos(FOUR_PI * (double)x / denominator)) -
(a3 * Math.cos(SIX_PI * (double)x / denominator)) +
(a4 * Math.cos(EIGHT_PI * (double)x / denominator));
}
return coefficients;
}
/**
* Calculates the beta (roll-off factor) for a Kaiser window based on the specified attenuation.
*
* @param attenuation in decibels
* @return beta value in the range of 0.0 to 1.0
*/
public static double getKaiserBeta(double attenuation)
{
if(attenuation > 50.0)
{
return 0.1102 * (attenuation - 8.7);
}
else if(attenuation >= 21.0)
{
return (0.5842 * Math.pow(attenuation - 21.0, 0.4)) + (0.07886 * (attenuation - 21.0));
}
else
{
return 0.0;
}
}
/**
* Creates a Kaiser window.
*
* @param length of the window
* @param attenuation desired
* @return window
*/
public static double[] getKaiser(int length, double attenuation)
{
double[] coefficients = new double[length];
double beta = getKaiserBeta(attenuation);
double betaBesselZerothOrder = getBesselZerothOrder(beta);
double temp;
for(int x = 0; x < coefficients.length; x++)
{
temp = beta * Math.sqrt(1.0 - Math.pow(2.0 * x / (length - 1) - 1.0, 2));
coefficients[x] = getBesselZerothOrder(temp) / betaBesselZerothOrder;
}
return coefficients;
}
/**
* Zeroth order modified Bessel function.
*
* @param x Value.
* @return Return value.
*/
public final static double getBesselZerothOrder(final double x)
{
double f = 1;
final double x2 = x * x * 0.25;
double xc = x2;
double v = 1 + x2;
for (int i = 2; i < 100; i++)
{
f *= i;
xc *= x2;
final double a = xc / (f * f);
v += a;
if (a < 1e-20) break;
}
return v;
}
/**
* Apply the window against an array of float-type samples
*/
public static float[] apply( double[] coefficients, float[] samples )
public static float[] apply(double[] coefficients, float[] samples)
{
for( int x = 0; x < coefficients.length; x++ )
{
samples[ x ] = (float)( samples[ x ] * coefficients[ x ] );
}
return samples;
}
public static float[] apply( WindowType type, float[] samples )
{
double[] coefficients = getWindow( type, samples.length );
return apply( coefficients, samples );
}
/**
* Apply the window against an array of float-type samples
*/
public static double[] apply( double[] coefficients, double[] samples )
{
for( int x = 0; x < coefficients.length; x++ )
{
samples[ x ] = samples[ x ] * coefficients[ x ];
}
return samples;
for(int x = 0; x < coefficients.length; x++)
{
samples[x] = (float)(samples[x] * coefficients[x]);
}
return samples;
}
public static double[] apply( WindowType type, double[] samples )
/**
* Apply the window type against the float array of samples
*
* @param type of window to use
* @param samples to be windowed
* @return windowed samples
*/
public static float[] apply(WindowType type, float[] samples)
{
double[] coefficients = getWindow( type, samples.length );
return apply( coefficients, samples );
double[] coefficients = getWindow(type, samples.length);
return apply(coefficients, samples);
}
/**
* Apply the window against an array of double-type samples
*/
public static double[] apply(double[] coefficients, double[] samples)
{
for(int x = 0; x < coefficients.length; x++)
{
samples[x] = samples[x] * coefficients[x];
}
return samples;
}
/**
* Apply the window type against the double array of samples
*
* @param type of window to use
* @param samples to be windowed
* @return windowed samples
*/
public static double[] apply(WindowType type, double[] samples)
{
double[] coefficients = getWindow(type, samples.length);
return apply(coefficients, samples);
}
/**
* Window types
*/
public enum WindowType
{
BLACKMAN, COSINE, HAMMING, HANNING, NONE;
}
/**
* Utility to log the double arrays
*/
public static String arrayToString( double[] array, boolean breaks )
{
StringBuilder sb = new StringBuilder();
for( int x = 0; x < array.length; x++ )
{
sb.append( x + ":" + array[ x ] );
BLACKMAN("Blackman"),
BLACKMAN_HARRIS_4("Blackman-Harris 4"),
BLACKMAN_HARRIS_7("Blackman-Harris 7"),
BLACKMAN_NUTALL("Blackman-Nutall"),
COSINE("Cosine"),
FLAT_TOP("Flat Top"),
HAMMING("Hamming"),
HANN("Hann"),
KAISER("Kaiser"),
NUTALL("Nutall"),
NONE("None");
if( breaks )
{
sb.append( "\n" );
}
else
{
if( x % 8 == 7 )
{
sb.append( "\n" );
}
else
{
sb.append( "\t" );
}
}
}
return sb.toString();
}
private String mLabel;
WindowType(String label)
{
mLabel = label;
}
public String toString()
{
return mLabel;
}
}
public static void main(String[] args)
{
double[] kaiser = getKaiser(16, 80.0);
mLog.debug("Window:" + Arrays.toString(kaiser));
}
}

View File

@@ -0,0 +1,170 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBufferQueue;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
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;
public abstract class AbstractComplexPolyphaseChannelizer implements Listener<ReusableComplexBuffer>, ISourceEventListener
{
private final static Logger mLog = LoggerFactory.getLogger(AbstractComplexPolyphaseChannelizer.class);
private ReusableChannelResultsBufferQueue mBufferQueue = new ReusableChannelResultsBufferQueue("AbstractComplexPolyphaseChannelizer");
private Broadcaster<SourceEvent> mSourceChangeBroadcaster = new Broadcaster();
private List<PolyphaseChannelSource> mChannels = new CopyOnWriteArrayList<>();
private double mSampleRate;
private int mChannelCount;
private int mSubChannelCount;
private double mChannelSampleRate;
/**
* Complex sample polyphase channelizer
*
* @param channelCount
*/
public AbstractComplexPolyphaseChannelizer(double sampleRate, int channelCount)
{
mChannelCount = channelCount;
mSubChannelCount = channelCount * 2; //Number of I/Q channels
mSampleRate = sampleRate;
mChannelSampleRate = (double)mSampleRate / (double)mChannelCount;
}
/**
* Input sample rate for this channelizer
* @return sample rate in hertz
*/
public double getSampleRate()
{
return mSampleRate;
}
/**
* Sets the input sample rate for for this channelizer
* @param sampleRate in hertz
*/
public void setRates(double sampleRate, int channelCount)
{
mSampleRate = sampleRate;
mChannelCount = channelCount;
mChannelSampleRate = mSampleRate / (double)mChannelCount;
}
/**
* Number of channels being processed by this channelizer.
*/
public int getChannelCount()
{
return mChannelCount;
}
/**
* Number of sub channels (I/Q) being processed by this channelizer. This is 2 * channel count.
*/
public int getSubChannelCount()
{
return mSubChannelCount;
}
/**
* Output channel sample rate
* @return sample rate in hertz
*/
public double getChannelSampleRate()
{
return mChannelSampleRate;
}
/**
* Dispatches the processed channel samples to any registered polyphase channel outputs.
*
* @param channelResultsBuffer containing an array of an array of I/Q samples per channel
*/
protected void dispatch(ReusableChannelResultsBuffer channelResultsBuffer)
{
for(PolyphaseChannelSource channel : mChannels)
{
channelResultsBuffer.incrementUserCount();
channel.receiveChannelResults(channelResultsBuffer);
}
channelResultsBuffer.decrementUserCount();
}
/**
* Adds the polyphase channel source to receive processed output channel samples
*
* @param polyphaseChannelSource
*/
public void addChannel(PolyphaseChannelSource polyphaseChannelSource)
{
if(polyphaseChannelSource != null && !mChannels.contains(polyphaseChannelSource))
{
mChannels.add(polyphaseChannelSource);
mSourceChangeBroadcaster.addListener(polyphaseChannelSource.getSourceEventListener());
}
}
/**
* Removes the polyphase channel source from receiving output channel samples.
*
* @param polyphaseChannelSource
*/
public void removeChannel(PolyphaseChannelSource polyphaseChannelSource)
{
if(polyphaseChannelSource != null && mChannels.contains(polyphaseChannelSource))
{
mChannels.remove(polyphaseChannelSource);
mSourceChangeBroadcaster.removeListener(polyphaseChannelSource.getSourceEventListener());
}
}
/**
* Number of polyphase channels registered to receive sample streams
*/
public int getRegisteredChannelCount()
{
return mChannels.size();
}
/**
* Creates or reuses a channel results buffer for subclass implementations to use for temporary
* storage of channel results through distribution and consumption of channel results by
* channel output processors.
*/
protected ReusableChannelResultsBuffer getChannelResultsBuffer()
{
return mBufferQueue.getBuffer();
}
@Override
public Listener<SourceEvent> getSourceEventListener()
{
return mSourceChangeBroadcaster;
}
}

View File

@@ -0,0 +1,557 @@
/*******************************************************************************
* *********************************************************************************************************************
* sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by 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.source.tuner.channel.TunerChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class ChannelCalculator
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelCalculator.class);
/**
* Policy to determine desired corrective action for methods that calculate an index for a frequency or a frequency
* from an index where the frequency may exist either at the overlapping boundary of two indexes, or in the middle
* of the wrap-around (N/2) index.
*/
public enum IndexBoundaryPolicy
{
ADJUST_POSITIVE,
ADJUST_NEGATIVE
}
private double mSampleRate;
private int mChannelCount;
private double mCenterFrequency;
private double mChannelBandwidth;
private double mOversampling = 2.0;
/**
* Calculates channel index(es) from a polyphase channelizer output to source a DDC polyphase channel source
* with a specified channel center frequency and bandwidth.
*
* Polyphase channel indexes are not linear/consecutive owing to the use of the FFT to rotate channels to their
* center frequencies following filtering in the polyphase filter bank. As such, this calculator provides methods
* to determine the min/center/max frequency of each channel that is adjusted for the current center frequency
* value and to determine the channel index(es) that need to be targeted to produce a requested output channel
* with a specific frequency and bandwdith.
*
* @param sampleRate in hertz for the input sample rate from the sample source (ie tuner)
* @param channelCount number of channels in the polyphase filter bank
* @param centerFrequency center frequency for the input sample stream
* @param oversampling set to 1.0 for no oversample, or greater than 1.0 for oversampling
*/
public ChannelCalculator(double sampleRate, int channelCount, double centerFrequency, double oversampling)
{
mSampleRate = sampleRate;
mChannelCount = channelCount;
mCenterFrequency = centerFrequency;
mOversampling = oversampling;
updateChannelBandwidth();;
}
/**
* Calculates the channel bandwidth from the sample rate and channel count.
*/
private void updateChannelBandwidth()
{
mChannelBandwidth = (double)mSampleRate / (double)mChannelCount;
}
/**
* Center frequency for the channelizer
*/
public double getCenterFrequency()
{
return mCenterFrequency;
}
/**
* Updates the center frequency for the channelizer
*/
public void setCenterFrequency(double frequency)
{
mCenterFrequency = frequency;
}
/**
* Number of channels being channelized by the polyphase channelizer
*/
public int getChannelCount()
{
return mChannelCount;
}
/**
* Updates the number of channels being channelized by the polyphase channelizer
*/
public void setChannelCount(int channelCount)
{
mChannelCount = channelCount;
updateChannelBandwidth();
}
/**
* Identifies the index of the wrap-around channel meaning that the lower half of the channel exists at the upper
* end of the spectrum and the upper half of the channel exists at the lower end of the spectrum
*
* @return wrap-around channel index
*/
public int getWrapAroundIndex()
{
return mChannelCount / 2;
}
/**
* PolyphaseChannelManager input sample rate
* @return input sample rate in hertz
*/
public double getSampleRate()
{
return mSampleRate;
}
/**
* Updates the channel count using the sample rate argument to calculate the number of channels from the
* already established channel bandwidth.
*
* @param sampleRate to use for channel count
*/
public void setRates(double sampleRate, int channelCount)
{
mSampleRate = sampleRate;
mChannelCount = channelCount;
updateChannelBandwidth();
}
/**
* Channel bandwidth. Note: this value may be smaller than the channel sample rate when oversampling
* is being used.
*/
public double getChannelBandwidth()
{
return mChannelBandwidth;
}
/**
* Half of the channel bandwidth.
*/
public double getHalfChannelBandwidth()
{
return mChannelBandwidth / 2.0;
}
/**
* Oversampling rate produced by the channelizer.
*
* @return 1.0 for no oversampling, or a value greater than 1.0 when oversampling is applied.
*/
public double getOversampling()
{
return mOversampling;
}
/**
* Sets the oversampling rate being applied by the polyphase channelizer
* @param oversampling rate where 1.0 indicates no oversampling, and a value greater than 1.0 indicates
* that the polyphase channelizer is oversampling each channel.
*/
public void setOversampling(double oversampling)
{
mOversampling = oversampling;
}
/**
* Channel sample rate. This value is the product of the filtered channel bandwidth times the
* oversampling rate.
*/
public double getChannelSampleRate()
{
return getChannelBandwidth() * getOversampling();
}
/**
* PolyphaseChannelManager input minimum frequency
* @return minimum frequency in hertz
*/
public long getMinimumFrequency()
{
return (long)(getCenterFrequency() - (getSampleRate() / 2.0));
}
/**
* PolyphaseChannelManager input maximum frequency
* @return maximum frequency in hertz
*/
public long getMaximumFrequency()
{
return (long)(getCenterFrequency() + (getSampleRate() / 2.0));
}
/**
* Identifies the channelizer channel index(es) required to produce a tuner channel with the specified center
* frequency and channel bandwidth.
*
* @param tunerChannel specifying the desired channel frequency and bandwidth
*
* @return an array of one or more polyphase channelizer index(es) to target in order to produce an output channel
* for the desired frequency and bandwidth.
*
* @throws IllegalArgumentException if the tuner channel's minimum or maximum frequency is not contained
* within the channelizer's minimum or maximum frequency range
*/
public List<Integer> getChannelIndexes(TunerChannel tunerChannel) throws IllegalArgumentException
{
if(tunerChannel.getMinFrequency() < getMinimumFrequency() ||
tunerChannel.getMaxFrequency() > getMaximumFrequency())
{
throw new IllegalArgumentException("Requested channel cannot be provided by this " +
"channelizer. Requested channel [" + tunerChannel.getMinFrequency() + " - " +
tunerChannel.getMaxFrequency() + "] exceeds current channelizer frequency range [" +
getMinimumFrequency() + " - " + getMaximumFrequency() + "]");
}
int minIndex = getIndexForFrequency(tunerChannel.getMinFrequency(), IndexBoundaryPolicy.ADJUST_POSITIVE);
int maxIndex = getIndexForFrequency(tunerChannel.getMaxFrequency(), IndexBoundaryPolicy.ADJUST_NEGATIVE);
//Detect the unsourceable situation where the wrap around index would be included in both the minimum index
//and the maximum index, meaning that the tuner channel bandwidth is nearly the same as the input sample rate.
if(minIndex == getWrapAroundIndex() && maxIndex == getWrapAroundIndex())
{
throw new IllegalArgumentException("Requested tuner channel cannot be provided. Requested bandwidth is " +
"within two channel bandwidths of the sample rate.");
}
List<Integer> indexes = new ArrayList<>();
indexes.add(minIndex);
if(minIndex != maxIndex)
{
//Ensure our min/max indexes are within the correct bounds 0 <> N-1 to safeguard the while loop below
if(minIndex < 0 || minIndex >= getChannelCount() || maxIndex < 0 || maxIndex >= getChannelCount())
{
throw new IllegalArgumentException("Something went wrong while calculating the min and max polyphase " +
"channel indexes for tuner channel: " + tunerChannel);
}
int pointer = minIndex + 1;
if(pointer >= getChannelCount())
{
pointer -= getChannelCount();
}
while(pointer != maxIndex)
{
indexes.add(pointer);
pointer++;
if(pointer >= getChannelCount())
{
pointer -= getChannelCount();
}
}
indexes.add(maxIndex);
}
return indexes;
}
/**
* Calculates the index for the specified frequency. Certain frequencies can exist as both the maximum frequency
* of one channel and the minimum frequency of an adjacent channel. The index boundary policy identifies your
* choice to select the higher frequency channel index (ADJUST_POSITIVE) or the lower frequency channel
* index (ADJUST_NEGATIVE).
*
* @param frequency to determine channel index for
* @param indexBoundaryPolicy to determine the behavior when the frequency overlaps two channel boundaries
* @return index for the frequency
*/
public int getIndexForFrequency(long frequency, IndexBoundaryPolicy indexBoundaryPolicy)
{
double offset = frequency - getCenterFrequency();
if(Math.abs(offset) < getHalfChannelBandwidth())
{
return 0; //Center index
}
if(offset > 0)
{
offset += getHalfChannelBandwidth();
}
else
{
offset -= getHalfChannelBandwidth();
}
int indexOffset = (int)(offset / getChannelBandwidth());
if(indexOffset < 0)
{
indexOffset += getChannelCount();
}
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE &&
isOverlapFrequency(frequency, indexOffset, indexOffset + 1))
{
indexOffset = normalize(indexOffset + 1);
}
else if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_NEGATIVE &&
isOverlapFrequency(frequency, indexOffset - 1, indexOffset))
{
indexOffset = normalize(indexOffset - 1);
}
return indexOffset;
}
/**
* Indicates if the frequency is an overlapping frequency between channel index 1 and channel index 2 where the
* maximum frequency of index 1 is the same as the minimum frequency of index 2 and both of these frequencies are
* the same as the frequency argument.
*
* Note: both index 1 and index 2 will be normalized to the range of 0 <> N-1 prior to determining overlap
*
* @param frequency to test for overlap between index 1 and 2
* @param index1 which is the lower channel index located next to index 2.
* @param index2 which is the higher channel index located next to index 1.
* @return true if frequency = index1 maximum frequency = index2 minimum frequency
*/
public boolean isOverlapFrequency(long frequency, int index1, int index2)
{
index1 = normalize(index1);
index2 = normalize(index2);
//Check that these two indexes are side by side
int delta = normalize(index2 - index1);
if(delta != 1)
{
return false;
}
long index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_POSITIVE);
long index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_NEGATIVE);
if(index1 == getWrapAroundIndex())
{
index1MaximumFrequency = (long)getIndexMaximumFrequency(index1, IndexBoundaryPolicy.ADJUST_NEGATIVE);
}
if(index2 == getWrapAroundIndex())
{
index2MinimumFrequency = (long)getIndexMinimumFrequency(index2, IndexBoundaryPolicy.ADJUST_POSITIVE);
}
return frequency == index1MaximumFrequency && frequency == index2MinimumFrequency;
}
/**
* Normalizes the index to the range of 0 <> N-1 channel indexes</>
*/
private int normalize(int index)
{
while(index < 0)
{
index += getChannelCount();
}
while(index >= getChannelCount())
{
index -= getChannelCount();
}
return index;
}
/**
* Calculates the center frequency for the channel identified by the index in the channel results array.
*
* @param index to determine the channel center frequency
* @return center frequency for the indexed channel
*/
public double getIndexCenterFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
{
if(index < 0 || index >= getChannelCount())
{
throw new IllegalArgumentException("Illegal channel index [" + index + "] current max channels[" +
getChannelCount() + "]");
}
int wrapAroundIndex = getWrapAroundIndex();
if(index == wrapAroundIndex)
{
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
{
return getCenterFrequency() + (index * getChannelBandwidth());
}
else
{
return getCenterFrequency() - (index * getChannelBandwidth());
}
}
else if(index < wrapAroundIndex)
{
return getCenterFrequency() + (index * getChannelBandwidth());
}
else
{
return getCenterFrequency() - ((getChannelCount() - index) * getChannelBandwidth());
}
}
/**
* Calculates the minimum frequency band edge for the channel identified by the index in the channel results
* array.
* @param index to calculate the minimum band edge frequency for the channel.
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the minimum
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the minimum frequency of the
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
* @return frequency of the minimum band edge
*/
public double getIndexMinimumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
{
if(index < 0 || index >= getChannelCount())
{
throw new IllegalArgumentException("Can't determine minimum frequency - illegal channel index [" +
index + "]");
}
int wrapAroundIndex = getWrapAroundIndex();
if(index == wrapAroundIndex)
{
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
{
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
}
else
{
return getIndexCenterFrequency(index, indexBoundaryPolicy);
}
}
else if(index <= wrapAroundIndex)
{
return getCenterFrequency() + ((double)index * getChannelBandwidth()) - getHalfChannelBandwidth();
}
else
{
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) -
getHalfChannelBandwidth();
}
}
/**
* Calculates the maximum frequency band edge for the channel identified by the index in the channel results
* array.
* @param index to calculate the maximum band edge frequency for the channel.
* @param indexBoundaryPolicy for the wrap around index to indicate if this method should return the maximum
* frequency of the upper spectrum half of the wrap around channel (ADJUST_POSITIVE) or the maximum frequency of the
* lower spectrum half of the wrap around channel (ADJUST_NEGATIVE)
* @return frequency of the maximum band edge
*/
public double getIndexMaximumFrequency(int index, IndexBoundaryPolicy indexBoundaryPolicy)
{
if(index < 0 || index >= getChannelCount())
{
throw new IllegalArgumentException("Can't determine maximum frequency - illegal channel index [" +
index + "]");
}
int wrapAroundIndex = getWrapAroundIndex();
if(index == wrapAroundIndex)
{
if(indexBoundaryPolicy == IndexBoundaryPolicy.ADJUST_POSITIVE)
{
return getIndexCenterFrequency(index, indexBoundaryPolicy);
}
else
{
return getCenterFrequency() - ((double)index * getChannelBandwidth() - getHalfChannelBandwidth());
}
}
else if(index <= wrapAroundIndex)
{
return getCenterFrequency() + ((double)index * getChannelBandwidth()) + getHalfChannelBandwidth();
}
else
{
return getCenterFrequency() - ((double)(getChannelCount() - index) * getChannelBandwidth()) +
getHalfChannelBandwidth();
}
}
/**
* Calculates the center frequency for a channel that will be synthesized from the channel indexes.
* @param indexes that are contiguous in the spectrum
* @return center frequency in hertz
*/
public long getCenterFrequencyForIndexes(List<Integer> indexes)
{
if(indexes.isEmpty())
{
throw new IllegalArgumentException("Indexes cannot be empty");
}
int centerIndex = (indexes.size() - 1) / 2;
int index = indexes.get(centerIndex);
if(indexes.size() % 2 == 0)
{
return (long)getIndexMaximumFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE);
}
else
{
if(index == getWrapAroundIndex())
{
return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_NEGATIVE);
}
else
{
return (long)getIndexCenterFrequency(index, IndexBoundaryPolicy.ADJUST_POSITIVE);
}
}
}
/**
* String representation of the channel calculator settings
*/
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Channel Calculator Settings");
sb.append(" Channels:").append(getChannelCount());
sb.append(" Sample Rate:").append(getSampleRate());
sb.append(" Channel Bandwidth:").append(getChannelBandwidth());
sb.append(" Channel Rate:").append(getChannelSampleRate());
sb.append(" Min:").append(getMinimumFrequency());
sb.append(" Max:").append(getMaximumFrequency());
return sb.toString();
}
}

View File

@@ -0,0 +1,457 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Non-Maximally Decimated Polyphase Filter Bank (NMDPFB) channelizer that divides the input baseband complex sample
* stream into equal bandwidth channels that are each oversampled by 2x for output.
*
* This polyphase channelizer is based off of the channelizer described by Fred Harris in Multirate Signal
* Processing for Communications Systems, p230-233.
*
* Samples are loaded into this filter one block at a time (1/2 channel count) and a filtered output is calculated
* to produce an overall 2x oversampled channel sample rate. Each sample block load is preceded by a serpentine
* shift of the existing sample blocks. We use the java System.arrayCopy() method which is able to leverage
* native processor intrinsics for efficiency.
*
* The prototype filter for the channelizer is rearranged to align with the structure of the sample buffer.
*
* Instead of using an array of channel filters as described in the Harris text, this filter and the sample buffer
* are arranged as a contiguous array to maximize Java's ability to leverage native processor Single Instruction
* Multiple Data (SIMD) intrinsics (since Java 8). The filter process is broken into four steps:
*
* -Multiply the inline array of samples and filter coefficients
* -Accumulate the results for each sub-channel
* -Rearrange the sub-channel results to correctly order the sub-channels
* -Perform IFFT
*
* Note: design the prototype filter as a Nyquist windowed filter with a -6.02 db attenuation at the channel edge
* frequency if you need Perfect Reconstruction where you'll later re-join two or more channels to form a wider
* bandwidth channel or to isolate a signal that located between two channels.
*/
public class ComplexPolyphaseChannelizerM2 extends AbstractComplexPolyphaseChannelizer
{
private final static Logger mLog = LoggerFactory.getLogger(ComplexPolyphaseChannelizerM2.class);
private static final int DEFAULT_MINIMUM_CHANNEL_BANDWIDTH = 12500;
//Sized at 152 buffers a second where max = 5 seconds and reset = 2 seconds worth of buffers
private IFFTProcessor mIFFTProcessor = new IFFTProcessor((5 * 152), (2 * 152));
private FloatFFT_1D mFFT;
private float[] mInlineSamples;
private float[] mInlineFilter;
private float[] mInlineInterimOutput;
private float[] mFilterAccumulator;
private boolean mTopBlockIndicator = true;
private int[] mTopBlockMap;
private int[] mMiddleBlockMap;
private int mSampleBufferPointer;
private int mSamplesPerBlock;
private int mTapsPerChannel;
/**
* Creates a NMDPFB channelizer instance.
*
* @param taps of a low-pass filter designed for the inbound sample rate with a cutoff frequency
* equal to the channel bandwidth (sample rate / filters). If you need to synthesize (combine two or more
* channel outputs) a new bandwidth signal from the outputs of this filter, then the filter should be designed
* as a nyquist filter with -6 dB attenuation at the channel bandwidth cutoff frequency
* @param sampleRate of the incoming sample stream
* @param channelCount - number of filters/channels to output. Since this filter bank performs 2x oversampling for
* each channel output, this number must be even (divisible by 2).
*/
public ComplexPolyphaseChannelizerM2(float[] taps, int sampleRate, int channelCount)
{
super(sampleRate, channelCount);
if(channelCount % 2 != 0)
{
throw new IllegalArgumentException("Channel count must be an even multiple of the over-sample rate (2x)");
}
mTapsPerChannel = (int)Math.ceil((double)taps.length / (double)channelCount);
init(taps);
}
/**
* Creates a NMDPFB channelizer instance and designs a Perfect Reconstruction prototype filter appropriate for
* the baseband sample rate and quantity of filter taps per polyphase sub-channel.
*
* @param sampleRate to be channelized.
* @param tapsPerChannel to use when designing the filter
*/
public ComplexPolyphaseChannelizerM2(double sampleRate, int tapsPerChannel) throws FilterDesignException
{
super(sampleRate, getChannelCount(sampleRate));
mTapsPerChannel = tapsPerChannel;
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
mTapsPerChannel, false);
init(filterTaps);
}
/**
* Starts sample processing
*/
public void start()
{
mIFFTProcessor.start();
}
/**
* Stops sample processing.
*/
public void stop()
{
mIFFTProcessor.stop();
}
/**
* Calculates the multiple of two number of channels that can be channelized from the specified sample rate so that
* each channel has a minimum bandwidth of the default channel bandwidth (12.5 kHz).
* @param sampleRate to channelize
* @return number of multiple of two channels that can be channelized.
*/
public static int getChannelCount(double sampleRate)
{
int channels = (int)(sampleRate / DEFAULT_MINIMUM_CHANNEL_BANDWIDTH);
if(channels % 2 != 0)
{
channels--;
}
mLog.debug("Sample Rate [" + sampleRate + "] providing [" + channels + "] channels at [" + (sampleRate / (double)channels) + "] Hz each");
return channels;
}
/**
* Updates this channelizer to use the new sample rate. This method creates a new filter suitable for the
* sample rate and reinitializes all internal data structures to prepare for processing the new sample rate.
* @param sampleRate in hertz
*/
@Override
public void setRates(double sampleRate, int channelCount)
{
try
{
super.setRates(sampleRate, channelCount);
float[] filterTaps = FilterFactory.getSincM2Channelizer(getChannelSampleRate(), getChannelCount(),
mTapsPerChannel, false);
init(filterTaps);
}
catch(FilterDesignException fde)
{
throw new IllegalArgumentException("Cannot create a channelizer filter for the specified sample rate [" +
sampleRate + "]");
}
}
/**
* Receives the complex sample buffer and processes the results through the channelizer.
*/
@Override
public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
ReusableChannelResultsBuffer channelResultsBuffer = getChannelResultsBuffer();
channelResultsBuffer.setTimestamp(reusableComplexBuffer.getTimestamp());
float[] samples = reusableComplexBuffer.getSamples();
int samplesPointer = 0;
int samplesToCopy;
while(samplesPointer < samples.length)
{
if(mSampleBufferPointer < mSamplesPerBlock)
{
samplesToCopy = mSamplesPerBlock - mSampleBufferPointer;
if((samples.length - samplesPointer) < samplesToCopy)
{
samplesToCopy = (samples.length - samplesPointer);
}
System.arraycopy(samples, samplesPointer, mInlineSamples, mSampleBufferPointer, samplesToCopy);
mSampleBufferPointer += samplesToCopy;
samplesPointer += samplesToCopy;
}
if(mSampleBufferPointer >= mSamplesPerBlock)
{
//Filter buffered samples and produce a single sample across each of the polyphase channels
process(channelResultsBuffer);
//Right-shift the samples in the buffer over to make room for a new block of samples
//Note: since JDK 8, hotspot JIT compiler uses native processor intrinsics for efficiency
System.arraycopy(mInlineSamples, 0, mInlineSamples, mSamplesPerBlock, (mInlineSamples.length - mSamplesPerBlock));
mSampleBufferPointer = 0;
}
}
//Enqueue the channel results buffer for IFFT processing and distribution on a different thread
mIFFTProcessor.receive(channelResultsBuffer);
//Decrement the user count to let the originator know we're done with their buffer
reusableComplexBuffer.decrementUserCount();
}
/**
* Creates a top-block processing accumulator map that maps each interim filter and sample index product
* to the corresponding final output index for the array that will feed the IFFT.
*
* @param channelCount - number of channels
* @return output index to filter accumulator index mapping
*/
private static int[] getTopBlockMap(int channelCount)
{
int[] newMap = new int[channelCount * 2];
//Reorder the subchannel arrays to the structure needed for top-block processing
int blockSize = channelCount / 2;
for(int channel = 0; channel < blockSize; channel++)
{
int newIndex = 2 * channel;
int originalIndex = 2 * (blockSize - channel - 1);
int offset = 2 * blockSize;
newMap[originalIndex] = newIndex;
newMap[originalIndex + 1] = newIndex + 1;
newMap[offset + originalIndex] = offset + newIndex;
newMap[offset + originalIndex + 1] = offset + newIndex + 1;
}
return newMap;
}
/**
* Creates a top-block processing accumulator map that maps each interim filter and sample index product
* to the corresponding final output index for the array that will feed the IFFT.
*
* @param channelCount - number of channels
* @return output index to filter accumulator index mapping
*/
private static int[] getMiddleBlockMap(int channelCount)
{
int[] newMap = new int[channelCount * 2];
//Reorder the subchannel arrays to the structure needed for top-block processing
int blockSize = channelCount / 2;
for(int channel = 0; channel < blockSize; channel++)
{
int newIndex = 2 * channel;
int originalIndex = 2 * (blockSize - channel - 1);
int offset = 2 * blockSize;
newMap[offset + originalIndex] = newIndex;
newMap[offset + originalIndex + 1] = newIndex + 1;
newMap[originalIndex] = offset + newIndex;
newMap[originalIndex + 1] = offset + newIndex + 1;
}
return newMap;
}
/**
* Rearranges the filter coefficients to align with a contiguous sample buffer for processing efficiency.
* @param coefficients of the polyphase filter
* @param channelCount number of channels where each channel is an I/Q pair
* @return filter rearranged for inline sample buffer processing
*/
private static float[] getAlignedFilter(float[] coefficients, int channelCount, int tapsPerChannel)
{
float[] filter = new float[channelCount * tapsPerChannel * 2];
int blockSize = channelCount;
int coefficientPointer = 0;
int filterPointer = 0;
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
while(coefficientPointer < coefficients.length)
{
filter[filterPointer++] = coefficients[coefficientPointer];
filter[filterPointer++] = coefficients[coefficientPointer++];
}
//Swap each of the coefficients on block size boundaries
for(int x = 0; x < filter.length; x += blockSize)
{
for(int y = 0; y < blockSize / 2; y++)
{
int index1 = x + y;
int index2 = x + (blockSize - y - 1);
float temp = filter[index2];
filter[index2] = filter[index1];
filter[index1] = temp;
}
}
return filter;
}
/**
* Processes the sample buffer for each new block of sample data that is loaded and distributes the results to any
* registered channel listeners.
*/
private void process(ReusableChannelResultsBuffer channelResultsBuffer)
{
//Multiply each of the samples by the corresponding filter tap
for(int x = 0; x < mInlineSamples.length; x++)
{
mInlineInterimOutput[x] = mInlineSamples[x] * mInlineFilter[x];
}
Arrays.fill(mFilterAccumulator, 0.0f);
int tapOffset = 0;
//Accumulate the sample/filter product results into each of the I/Q sub-channels
for(int tap = 0; tap < mTapsPerChannel; tap++)
{
tapOffset = tap * getSubChannelCount();
for(int channel = 0; channel < getSubChannelCount(); channel++)
{
mFilterAccumulator[channel] += mInlineInterimOutput[tapOffset + channel];
}
}
float[] processed = channelResultsBuffer.getEmptyBuffer(getSubChannelCount());
if(mTopBlockIndicator)
{
for(int x = 0; x < getSubChannelCount(); x++)
{
processed[x] = mFilterAccumulator[mTopBlockMap[x]];
}
}
else
{
for(int x = 0; x < getSubChannelCount(); x++)
{
processed[x] = mFilterAccumulator[mMiddleBlockMap[x]];
}
}
channelResultsBuffer.addChannelResults(processed);
mTopBlockIndicator = !mTopBlockIndicator;
}
/**
* Initializes the channelizer filter structures.
*
* @param coefficients of the prototype filter for this channelizer
*/
private void init(float[] coefficients)
{
mFFT = new FloatFFT_1D(getChannelCount());
int channelCount = getChannelCount();
int bufferLength = getSubChannelCount() * mTapsPerChannel;
mSamplesPerBlock = getChannelCount(); //Same as subChannelCount / 2
mTopBlockMap = getTopBlockMap(channelCount);
mMiddleBlockMap = getMiddleBlockMap(channelCount);
mInlineFilter = getAlignedFilter(coefficients, channelCount, mTapsPerChannel);
mInlineSamples = new float[bufferLength];
mInlineInterimOutput = new float[bufferLength];
mFilterAccumulator = new float[getSubChannelCount()];
}
/**
* Separate threaded processor to receive and enqueue filtered channel results buffers, perform IFFT on each array
* as required to align the phase of each polyphase channel, and then dispatch the results to any registered
* sample consumer channels.
*/
public class IFFTProcessor extends ContinuousBufferProcessor<ReusableChannelResultsBuffer>
{
public IFFTProcessor(int maximumSize, int resetThreshold)
{
super(maximumSize, resetThreshold);
//We create a listener interface to receive the buffers from the scheduled thread pool
//dispatcher thread that is part of this continuous buffer processor. We perform an IFFT on each
//channel results array contained in each results buffer and then dispatch the buffer
//so that it can be distributed to each channel listener.
setListener(new Listener<List<ReusableChannelResultsBuffer>>()
{
@Override
public void receive(List<ReusableChannelResultsBuffer> buffers)
{
for(ReusableChannelResultsBuffer buffer: buffers)
{
for(float[] channelResults: buffer.getChannelResults())
{
//Rotate each of the channels to the correct phase using the IFFT
mFFT.complexInverse(channelResults, true);
}
dispatch(buffer);
}
}
});
setOverflowListener(new IOverflowListener()
{
@Override
public void sourceOverflow(boolean overflow)
{
mLog.debug("IFFTProcessor overflow changed - overflow:" + overflow);
}
});
}
/**
* Clears any buffers from the dispatch/processing queue. Overrides the parent method so that we can set
* the user count to 0 to allow the buffer to be reclaimed.
*/
protected Collection<ReusableChannelResultsBuffer> clearQueue()
{
Collection<ReusableChannelResultsBuffer> buffersToDispose = super.clearQueue();
for(ReusableChannelResultsBuffer buffer: buffersToDispose)
{
buffer.clearUserCount();
}
return buffersToDispose;
}
}
}

View File

@@ -0,0 +1,208 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by 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.sample.IOverflowListener;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.OverflowableTransferQueue;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class ContinuousBufferProcessor<E> implements Listener<E>
{
private final static Logger mLog = LoggerFactory.getLogger(ContinuousBufferProcessor.class);
private OverflowableTransferQueue<E> mQueue;
private Listener<List<E>> mListener;
private ScheduledFuture<?> mScheduledFuture;
private AtomicBoolean mRunning = new AtomicBoolean();
/**
* Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task
* for periodically distributing internally queued elements to the registered listener. This processor provides
* a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
* processing workload.
*
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
*
* @param maximumSize of the internal queue (overflow happens when this is exceeded)
* @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold
*/
public ContinuousBufferProcessor(int maximumSize, int resetThreshold)
{
this(new OverflowableTransferQueue<>(maximumSize, resetThreshold));
}
/**
* Scheduled Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing task
* for periodically distributing internally queued elements to the registered listener. This processor provides
* a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
* processing workload.
*
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
*
* @param queue implmentation of an overflowable transfer queue
*/
public ContinuousBufferProcessor(OverflowableTransferQueue<E> queue)
{
mQueue = queue;
}
/**
* Sets the listener to receive notifications of buffer overflow and/or reset. Note: this method can also be
* invoked with a null argument to clear the previously registered overflow listener.
*
* @param listener to receive overflow/reset notifications.
*/
public void setOverflowListener(IOverflowListener listener)
{
mQueue.setOverflowListener(listener);
}
/**
* Sets or changes the listener to receive buffers from this processor.
* @param listener to receive buffers
*/
public void setListener(Listener<List<E>> listener)
{
mListener = listener;
}
/**
* Primary input method for adding buffers to this processor. Note: incoming buffers will be ignored if this
* processor is in a stopped state. You must invoke start() to allow incoming buffers and initiate buffer
* processing.
*
* @param e to enqueue for distribution to a registered listener
*/
@Override
public void receive(E e)
{
mQueue.offer(e);
}
/**
* Starts this buffer processor and allows queuing of incoming buffers.
*/
public void start()
{
if(mRunning.compareAndSet(false, true))
{
mScheduledFuture = ThreadPool.SCHEDULED.scheduleAtFixedRate(new Processor(), 0, 5, TimeUnit.MILLISECONDS);
}
}
/**
* Stops this buffer processor, stops queuing of incoming buffers and clears the internal buffer queue.
*/
public void stop()
{
if(mRunning.compareAndSet(true, false))
{
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
mScheduledFuture = null;
}
clearQueue();
}
}
/**
* Stops this buffer processor,stops queuing of incoming buffers and flushes the internal buffer queue
* contents to the listener.
*/
public void flushAndStop()
{
if(mRunning.compareAndSet(true, false))
{
if(mScheduledFuture != null)
{
mScheduledFuture.cancel(true);
mScheduledFuture = null;
}
process();
}
}
/**
* Distributes queued buffers to the listener
*/
private void process()
{
List<E> buffers = new ArrayList<>();
try
{
mQueue.drainTo(buffers);
if(mListener != null)
{
mListener.receive(buffers);
}
}
catch(Throwable throwable)
{
mLog.error("Error while dispatching buffers to listener", throwable);
}
}
/**
* Clears any buffers from the queue
*/
protected Collection<E> clearQueue()
{
Collection<E> clearedElements = new ArrayList<>();
mQueue.drainTo(clearedElements);
return clearedElements;
}
/**
* Indicates if this processor is currently running
*/
public boolean isRunning()
{
return mRunning.get();
}
/**
* Processor to service the buffer queue and distribute the buffers to the registered listener
*/
class Processor implements Runnable
{
@Override
public void run()
{
process();
}
}
}

View File

@@ -0,0 +1,41 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.sample.buffer.AbstractReusableBuffer;
import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue;
public class ContinuousReusableBufferProcessor<T extends AbstractReusableBuffer> extends ContinuousBufferProcessor<T>
{
/**
* Scheduled Reusable Buffer Processor combines an internal overflowable buffer with a scheduled runnable processing
* task for periodically distributing internally queued elements to the registered listener. This processor
* provides a convenient way to create a thread-safe buffer for receiving elements from one thread/runnable and then
* distributing those elements to a registered listener where distribution occurs on a separate scheduled thread
* pool runnable thread. This allows the calling input thread to quickly return without incurring any subsequent
* processing workload.
*
* The internal queue is an overflowable queue implementation that allows a listener to be registered to receive
* notifications of overflow and reset state. Queue sizing parameters are specified in the constructor.
*
* @param maximumSize of the internal queue (overflow happens when this is exceeded)
* @param resetThreshold of the internal queue (overflow reset happens once queue size falls below this threshold
*/
public ContinuousReusableBufferProcessor(int maximumSize, int resetThreshold)
{
super(new OverflowableReusableBufferTransferQueue<T>(maximumSize, resetThreshold));
}
}

View File

@@ -0,0 +1,606 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
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.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.IReusableComplexBufferProvider;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.source.ISourceEventProcessor;
import io.github.dsheirer.source.Source;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.TunerController;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
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;
/**
* Polyphase Channel Manager is a DDC channel manager and complex buffer queue/processor for a tuner. This class
* provides DDC polyphase channel sources and wraps a polyphase channelizer processing sample buffers produced by
* the tuner and distributing channelized sample buffers to each allocated DDC polyphase channel source. This
* class is responsible for monitoring the tuner for changes in center frequency and/or sample rate and updating
* active DDC polyphase channel sources accordingly. This class also monitors source event requests and
* notifications received from active DDC polyphase channel sources to adjust sample streams as required.
*
* Channel bandwidth and channel count are determined by the sample rate of the baseband buffer stream provider. This
* class is currently designed to provide channels each with a minimum usable bandwidth of 12.5 kHz and oversampled by
* 2.0 to a minimum of 25.0 kHz channel sample rate. If the baseband stream provider sample rate is not evenly
* divisible by 12.5 kHz channels for an even number of channels, the channel bandwidth will be increased.
*
* Note: add this channel manager as a source event listener to the complex buffer provider to ensure this manager
* adapts to changes in source frequency and sample rate.
*/
public class PolyphaseChannelManager implements ISourceEventProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelManager.class);
private static final double MINIMUM_CHANNEL_BANDWIDTH = 12500.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 IReusableComplexBufferProvider mReusableBufferProvider;
private List<PolyphaseChannelSource> mChannelSources = new CopyOnWriteArrayList<>();
private ChannelCalculator mChannelCalculator;
private ComplexPolyphaseChannelizerM2 mPolyphaseChannelizer;
private ChannelSourceEventListener mChannelSourceEventListener = new ChannelSourceEventListener();
private BufferSourceEventMonitor mBufferSourceEventMonitor = new BufferSourceEventMonitor();
private ContinuousBufferProcessor<ReusableComplexBuffer> mBufferProcessor;
private Map<Integer,float[]> mOutputProcessorFilters = new HashMap<>();
/**
* Creates a polyphase channel manager instance.
*
* @param reusableComplexBufferProvider (ie tuner) that supports register/deregister for reusable baseband sample buffer
* streams
* @param frequency of the baseband complex buffer sample stream (ie center frequency)
* @param sampleRate of the baseband complex buffer sample stream
*/
public PolyphaseChannelManager(IReusableComplexBufferProvider reusableComplexBufferProvider,
long frequency, double sampleRate)
{
if(reusableComplexBufferProvider == null)
{
throw new IllegalArgumentException("Complex buffer provider argument cannot be null");
}
mReusableBufferProvider = reusableComplexBufferProvider;
int channelCount = (int)(sampleRate / MINIMUM_CHANNEL_BANDWIDTH);
//Ensure channel count is an even integer since we're using a 2x oversampling polyphase channelizer
if(channelCount % 2 != 0)
{
channelCount--;
}
mChannelCalculator = new ChannelCalculator(sampleRate, channelCount, frequency, CHANNEL_OVERSAMPLING);
mBufferProcessor = new ContinuousBufferProcessor(200, 50);
mBufferProcessor.setListener(mBufferSourceEventMonitor);
}
/**
* Creates a polyphase channel manager for the tuner controller
*
* @param tunerController for a tuner that provides a baseband complex buffer stream.
*/
public PolyphaseChannelManager(TunerController tunerController)
{
this(tunerController, tunerController.getFrequency(), tunerController.getSampleRate());
}
/**
* Current channel sample rate which is (2 * channel bandwidth).
*/
public double getChannelSampleRate()
{
return mChannelCalculator.getChannelSampleRate();
}
/**
* Current channel bandwidth/spacing.
*/
public double getChannelBandwidth()
{
return mChannelCalculator.getChannelBandwidth();
}
/**
* Provides a Digital Drop Channel (DDC) for the specified tuner channel or returns null if the channel can't be
* sourced due to the current center frequency and/or sample rate.
* @param tunerChannel specifying center frequency and bandwidth.
* @return source or null.
*/
public TunerChannelSource getChannel(TunerChannel tunerChannel)
{
PolyphaseChannelSource channelSource = null;
try
{
List<Integer> polyphaseIndexes = mChannelCalculator.getChannelIndexes(tunerChannel);
IPolyphaseChannelOutputProcessor outputProcessor = getOutputProcessor(polyphaseIndexes);
if(outputProcessor != null)
{
long centerFrequency = mChannelCalculator.getCenterFrequencyForIndexes(polyphaseIndexes);
channelSource = new PolyphaseChannelSource(tunerChannel, outputProcessor, mChannelSourceEventListener,
mChannelCalculator.getChannelSampleRate(), centerFrequency);
mChannelSources.add(channelSource);
}
}
catch(IllegalArgumentException iae)
{
mLog.info("Can't provide DDC for " + tunerChannel.toString() + " due to channelizer frequency [" +
mChannelCalculator.getCenterFrequency() + "] and sample rate [" +
(mChannelCalculator.getChannelCount() * mChannelCalculator.getChannelBandwidth()) + "]");
}
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.
*
* @param channelSource to start
*/
private void startChannelSource(PolyphaseChannelSource channelSource)
{
synchronized(mBufferProcessor)
{
//Note: the polyphase channel source has already been added to the mChannelSources in getChannel() method
checkChannelizerConfiguration();
mPolyphaseChannelizer.addChannel(channelSource);
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
//If this is the first channel, register to start the sample buffers flowing
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 1)
{
mReusableBufferProvider.addBufferListener(mBufferProcessor);
mPolyphaseChannelizer.start();
mBufferProcessor.start();
}
}
}
/**
* Stops/removes the channel source from receiving channelized sample buffers and deregisters from the tuner
* when this is the last channel being sourced.
*
* @param channelSource to stop
*/
private void stopChannelSource(PolyphaseChannelSource channelSource)
{
synchronized(mBufferProcessor)
{
mChannelSources.remove(channelSource);
mPolyphaseChannelizer.removeChannel(channelSource);
mSourceEventBroadcaster.broadcast(SourceEvent.channelCountChange(getTunerChannelCount()));
//If this is the last/only channel, deregister to stop the sample buffers
if(mPolyphaseChannelizer.getRegisteredChannelCount() == 0)
{
mReusableBufferProvider.removeBufferListener(mBufferProcessor);
mBufferProcessor.stop();
mPolyphaseChannelizer.stop();
}
}
try
{
//Broadcast a stop sample stream notification in case this was a forced-stop so consumers are aware
channelSource.process(SourceEvent.stopSampleStreamNotification(channelSource));
}
catch(SourceException se)
{
//Do nothing
}
}
/**
* Process source events received from the source (ie tuner controller) for frequency and sample rate change
* notifications.
* @param sourceEvent to process
* @throws SourceException
*/
@Override
public void process(SourceEvent sourceEvent) throws SourceException
{
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
mBufferSourceEventMonitor.receive(sourceEvent);
break;
case NOTIFICATION_SAMPLE_RATE_CHANGE:
//Update channel calculator immediately so that channels can be allocated
double sampleRate = sourceEvent.getValue().doubleValue();
int channelCount = ComplexPolyphaseChannelizerM2.getChannelCount(sampleRate);
mChannelCalculator.setRates(sampleRate, channelCount);
break;
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_LOCKED:
case NOTIFICATION_FREQUENCY_AND_SAMPLE_RATE_UNLOCKED:
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//no-op
break;
default:
mLog.info("Unrecognized source event: " + sourceEvent.toString());
break;
}
}
/**
* Creates or updates the channelizer to process the incoming sample rate and updates any channel processors.
*
* Note: this method should only be invoked on the mBufferProcessor thread or prior to starting the mBufferProcessor.
* Sample rate source events will normally arrive via the incoming complex buffer stream from the mBufferProcessor
* and will be handled as they arrive.
*/
private void checkChannelizerConfiguration()
{
//Channel calculator is always in sync with the tuner's current sample rate
double tunerSampleRate = mChannelCalculator.getSampleRate();
//If the channelizer is not setup, or setup to the wrong sample rate, recreate it
if(mPolyphaseChannelizer == null || Math.abs(mPolyphaseChannelizer.getSampleRate() - tunerSampleRate) > 0.5)
{
if(mPolyphaseChannelizer != null && mPolyphaseChannelizer.getRegisteredChannelCount() > 0)
{
throw new IllegalStateException("Polyphase Channelizer cannot be changed to a new sample rate while " +
"channels are currently sourced. Ensure you remove all tuner channels before changing tuner " +
"sample rate. Current channel count:" +
(mPolyphaseChannelizer != null ? mPolyphaseChannelizer.getRegisteredChannelCount() : "0"));
}
try
{
mPolyphaseChannelizer = new ComplexPolyphaseChannelizerM2(tunerSampleRate,
POLYPHASE_CHANNELIZER_TAPS_PER_CHANNEL);
}
catch(IllegalArgumentException iae)
{
mLog.error("Could not create polyphase channelizer for sample rate [" + tunerSampleRate + "]", iae);
}
catch(FilterDesignException fde)
{
mLog.error("Could not create filter for polyphase channelizer for sample rate [" + tunerSampleRate + "]", fde);
}
//Clear any previous channel synthesis filters so they can be recreated for the new channel sample rate
mOutputProcessorFilters.clear();
}
}
/**
* 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)
{
for(PolyphaseChannelSource channelSource: mChannelSources)
{
updateOutputProcessor(channelSource);
//Send the non-null source event to each channel source
if(sourceEvent != null)
{
try
{
channelSource.process(sourceEvent);
}
catch(SourceException se)
{
mLog.error("Error while notifying polyphase channel source of a source event", se);
}
}
}
}
/**
* 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())
{
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");
}
}
}
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);
}
}
/**
* Sorted set of currently sourced tuner channels being provided by this channel manager. The set is ordered by
* frequency (low to high).
*/
public SortedSet<TunerChannel> getTunerChannels()
{
SortedSet<TunerChannel> tunerChannels = new TreeSet<>();
for(PolyphaseChannelSource channelSource: mChannelSources)
{
tunerChannels.add(channelSource.getTunerChannel());
}
return tunerChannels;
}
/**
* Count of currently sourced tuner channels
*/
public int getTunerChannelCount()
{
return mChannelSources.size();
}
/**
* Adds the listener to receive source events
*/
public void addSourceEventListener(Listener<SourceEvent> listener)
{
mSourceEventBroadcaster.addListener(listener);
}
/**
* Removes the listener from receiving source events
*/
public void removeSourceEventListener(Listener<SourceEvent> listener)
{
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
*/
private class ChannelSourceEventListener implements Listener<SourceEvent>
{
@Override
public void receive(SourceEvent sourceEvent)
{
switch(sourceEvent.getEvent())
{
case REQUEST_START_SAMPLE_STREAM:
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
{
startChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
}
else
{
mLog.error("Request to start sample stream for unrecognized source: " +
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
}
break;
case REQUEST_STOP_SAMPLE_STREAM:
if(sourceEvent.hasSource() && sourceEvent.getSource() instanceof PolyphaseChannelSource)
{
stopChannelSource((PolyphaseChannelSource)sourceEvent.getSource());
}
else
{
mLog.error("Request to stop sample stream for unrecognized source: " +
(sourceEvent.hasSource() ? sourceEvent.getSource().getClass() : "null source"));
}
break;
case REQUEST_SOURCE_DISPOSE:
Source source = sourceEvent.getSource();
if(source instanceof PolyphaseChannelSource)
{
source.dispose();
}
break;
case NOTIFICATION_FREQUENCY_CORRECTION_CHANGE:
//ignore
break;
case NOTIFICATION_MEASURED_FREQUENCY_ERROR_SYNC_LOCKED:
//Rebroadcast so that the tuner source can process this event
mSourceEventBroadcaster.broadcast(sourceEvent);
break;
default:
mLog.error("Received unrecognized source event from polyphase channel source [" +
sourceEvent.getEvent() + "]");
break;
}
}
}
/**
* Processes the incoming buffer stream from the provider and transfers the buffers to the polyphase channelizer.
*
* This monitor incorporates a queue for receiving source events that affect configuration of the channelizer so
* that they can be processed on the buffer processor calling thread, avoiding unnecessary locks on the channelizer
* and/or the channel sources and output processors.
*/
public class BufferSourceEventMonitor implements Listener<List<ReusableComplexBuffer>>
{
private Queue<SourceEvent> mQueuedSourceEvents = new ConcurrentLinkedQueue<>();
/**
* Queues the source event for deferred execution on the buffer processing thread.
* @param event that affects configuration of the channelizer (frequency or sample rate change events)
*/
public void receive(SourceEvent event)
{
mQueuedSourceEvents.offer(event);
}
@Override
public void receive(List<ReusableComplexBuffer> reusableComplexBuffers)
{
try
{
//Process any queued source events before processing the buffers
SourceEvent queuedSourceEvent = mQueuedSourceEvents.poll();
while(queuedSourceEvent != null)
{
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();
}
for(ReusableComplexBuffer reusableComplexBuffer: reusableComplexBuffers)
{
if(mPolyphaseChannelizer != null)
{
//User count management is handled by the channelizer
mPolyphaseChannelizer.receive(reusableComplexBuffer);
}
else
{
reusableComplexBuffer.decrementUserCount();
}
}
}
catch(Throwable throwable)
{
mLog.error("Error", throwable);
for(ReusableComplexBuffer buffer: reusableComplexBuffers)
{
buffer.decrementUserCount();
}
}
}
}
}

View File

@@ -0,0 +1,251 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.dsp.filter.channelizer.output.IPolyphaseChannelOutputProcessor;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.source.SourceEvent;
import io.github.dsheirer.source.tuner.channel.TunerChannel;
import io.github.dsheirer.source.tuner.channel.TunerChannelSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PolyphaseChannelSource extends TunerChannelSource
{
private final static Logger mLog = LoggerFactory.getLogger(PolyphaseChannelSource.class);
private ReusableComplexBufferAssembler mReusableComplexBufferAssembler;
private IPolyphaseChannelOutputProcessor mPolyphaseChannelOutputProcessor;
private double mChannelSampleRate;
private long mIndexCenterFrequency;
private long mChannelFrequencyCorrection;
/**
* Polyphase channelizer tuner channel source implementation. Adapts the channel array output samples from the
* polyphase channelizer into a single channel, or a channel synthesized from two adjacent channels that is
* frequency translated and decimated to a single channel.
*
* @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 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)
*/
public PolyphaseChannelSource(TunerChannel tunerChannel, IPolyphaseChannelOutputProcessor outputProcessor,
Listener<SourceEvent> producerSourceEventListener, double channelSampleRate,
long centerFrequency)
{
super(producerSourceEventListener, tunerChannel);
mPolyphaseChannelOutputProcessor = outputProcessor;
mChannelSampleRate = channelSampleRate;
mReusableComplexBufferAssembler = new ReusableComplexBufferAssembler(2500, mChannelSampleRate);
setFrequency(centerFrequency);
}
/**
* Registers the listener to receive complex sample buffers from this channel source
*/
@Override
public void setListener(final Listener<ReusableComplexBuffer> listener)
{
mReusableComplexBufferAssembler.setListener(listener);
}
/**
* Removes the listener from receiving complex sample buffers from this channel source
*/
@Override
public void removeListener(Listener<ReusableComplexBuffer> listener)
{
mReusableComplexBufferAssembler.setListener(null);
}
/**
* Channel output processor used by this channel source to convert polyphase channel results into a specific
* channel complex buffer output stream.
*/
public IPolyphaseChannelOutputProcessor getPolyphaseChannelOutputProcessor()
{
return mPolyphaseChannelOutputProcessor;
}
/**
* 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)
{
//Lock on the channel output processor to block the channel results processor thread from servicing the
//channel output processor's buffer while we change out the processors.
synchronized(mPolyphaseChannelOutputProcessor)
{
IPolyphaseChannelOutputProcessor existingProcessor = mPolyphaseChannelOutputProcessor;
//Swap out the processor so that incoming samples can accumulate in the new channel output processor
mPolyphaseChannelOutputProcessor = outputProcessor;
//Remove the buffer overflow listener and clear any previous buffer overflow state
existingProcessor.setSourceOverflowListener(null);
broadcastOverflowState(false);
//Register to receive buffer overflow notifications so we can push them up through the source chain
mPolyphaseChannelOutputProcessor.setSourceOverflowListener(this);
//Fully process the residual channel results buffer of the previous channel output processor
if(existingProcessor != null)
{
existingProcessor.processChannelResults(mReusableComplexBufferAssembler);
}
//Finally, setup the frequency offset for the output processor and reset the frequency correction value
//to allow consumers to adjust and calculate a new frequency correction value, if needed.
mIndexCenterFrequency = frequency;
mChannelFrequencyCorrection = 0;
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
}
}
/**
* Primary method for receiving channel results output from a polyphase channelizer. The results buffer will be
* queued for processing to extract the target channel samples, process them for frequency correction and/or
* channel aggregation, and dispatch the results to the downstream sample listener/consumer.
*
* @param channelResultsBuffer containing an array of polyphase channelizer outputs.
*/
public void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer)
{
mReusableComplexBufferAssembler.updateTimestamp(channelResultsBuffer.getTimestamp());
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.receiveChannelResults(channelResultsBuffer);
}
}
/**
* Downstream channel sample rate
*
* @return sample rate in Hertz
*/
@Override
public double getSampleRate()
{
return mChannelSampleRate;
}
/**
* Sets the downstream channel sample rate
* @param sampleRate in hertz
*/
@Override
protected void setSampleRate(double sampleRate)
{
mChannelSampleRate = sampleRate;
}
@Override
public void dispose()
{
if(mReusableComplexBufferAssembler != null)
{
mReusableComplexBufferAssembler.dispose();
}
if(mPolyphaseChannelOutputProcessor != null)
{
mPolyphaseChannelOutputProcessor.dispose();
mPolyphaseChannelOutputProcessor = null;
}
}
@Override
public long getChannelFrequencyCorrection()
{
return mChannelFrequencyCorrection;
}
/**
* Adjusts the frequency correction value that is being applied to the channelized output stream by the
* polyphase channel output processor.
*
* @param value to apply for frequency correction in hertz
*/
protected void setChannelFrequencyCorrection(long value)
{
mChannelFrequencyCorrection = value;
updateFrequencyOffset();
broadcastConsumerSourceEvent(SourceEvent.frequencyCorrectionChange(mChannelFrequencyCorrection));
}
/**
* Sets the center frequency for this channel.frequency for the incoming sample stream channel results and resets frequency correction to zero.
* @param frequency in hertz
*/
@Override
public void setFrequency(long frequency)
{
mIndexCenterFrequency = frequency;
//Set frequency correction to zero to trigger an update to the mixer and allow downstream monitors to
//recalculate the frequency error correction again
setChannelFrequencyCorrection(0);
}
@Override
protected void processSamples()
{
//Lock on the output processor so that it can't be changed in the middle of processing
synchronized(mPolyphaseChannelOutputProcessor)
{
mPolyphaseChannelOutputProcessor.processChannelResults(mReusableComplexBufferAssembler);
}
}
/**
* Calculates the frequency offset required to mix the incoming signal to center the desired frequency
* within the channel
*/
private long getFrequencyOffset()
{
return mIndexCenterFrequency - getTunerChannel().getFrequency() + mChannelFrequencyCorrection;
}
/**
* Updates the frequency offset being applied by the output processor. Calculates the offset using the center
* frequency of the incoming polyphase channel results and the desired output channel frequency adjusted by any
* requested frequency correction value.
*/
private void updateFrequencyOffset()
{
synchronized(mPolyphaseChannelOutputProcessor)
{
mPolyphaseChannelOutputProcessor.setFrequencyOffset(getFrequencyOffset());
}
}
@Override
public String toString()
{
return "POLYPHASE [" + mPolyphaseChannelOutputProcessor.getInputChannelCount() + "] " +
getTunerChannel().getFrequency();
}
}

View File

@@ -0,0 +1,83 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
public class SampleTimestampManager
{
private long mReferenceTimestamp = System.currentTimeMillis();
private double mSamplesPerMilliSecond;
private long mSampleCount;
/**
* Tracks the number of samples produced relative to a reference timestamp to derive a current reference timestamp
* for use in generating sample buffers.
*
* @param sampleRate per second
*/
public SampleTimestampManager(double sampleRate)
{
}
/**
* Updates the internal reference timestamp and resets the sample counter to zero.
*
* @param referenceTimestamp in milli-seconds since epoch
*/
public void setReferenceTimestamp(long referenceTimestamp)
{
mReferenceTimestamp = referenceTimestamp;
mSampleCount = 0;
}
/**
* Updates the sample rate that is used in calculating the reference timestamp
*
* @param sampleRate
*/
public void setSampleRate(double sampleRate)
{
//Convert to samples per milli-second for compatibility with reference timestamp
mSamplesPerMilliSecond = sampleRate / 1000.0d;
}
/**
* Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
*/
public void increment()
{
mSampleCount++;
}
/**
* Increments the internal count of dispatched samples to use in calculating an accurate reference timestamp.
*
* @param value to add to the sample count
*/
public void increment(int value)
{
mSampleCount += value;
}
public long getCurrentTimestamp()
{
if(mSampleCount > 0)
{
return mReferenceTimestamp + (long)((double)mSampleCount / mSamplesPerMilliSecond);
}
return mReferenceTimestamp;
}
}

View File

@@ -0,0 +1,191 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
import org.jtransforms.fft.FloatFFT_1D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements a two-channel polyphase filer synthesizer. This class is intended to be used with an M2 polyphase
* channelizer in order to recover a signal that overlaps the boundary between two channels. This synthesizer will
* rejoin the two M2 neighboring channels, mix the signal of interest to baseband, and filter the output stream to
* half of the sample rate, producing an overall M2 output sample rate.
*
* The synthesizer concept is modeled on the structure described by Fred Harris et al, in 'Interleaving Different
* Bandwidth Narrowband Channels in Perfect Reconstruction Cascade Polyphase Filter Banks for Efficient Flexible
* Variable Bandwidth Filters in Wideband Digital Transceivers'.
*
* The data buffers and sub-filter kernels bear some resemblance to the structures defined in the paper. The data and
* filter structures in this class are organized to take advantage of Java's ability to leverage SIMD processor
* intrinsics for optimal efficiency in processing. The paper specifies splitting the filter kernel into N polyphase
* partitions and creating N data buffers. This class organizes the data buffer as a contiguous sample
* array and creates an I/Q interleaved filter kernel. Instead of calculating the dot-product for each sub-filter,
* we calculate the product of the full data array against the filter and then accumulate each sub-filter. This allows
* java to use SIMD for the array product and then normal processing for the accumulation. Since this is a two-channel
* processor and the results of each filter accumulation are added, we use a single accumulator across both filters.
*
*/
public class TwoChannelSynthesizerM2
{
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelSynthesizerM2.class);
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("Two Channel Synthesizer M2");
private float[] mSerpentineDataBuffer;
private float[] mIQInterleavedFilter;
private float[] mFilterVectorProduct;
private float[] mIFFTBuffer = new float[4];
private float mIAccumulator;
private float mQAccumulator;
private FloatFFT_1D mFFT = new FloatFFT_1D(2);
private boolean mTopBlockFlag = true;
/**
* Polyphase synthesizer for mixing two M2 oversampled channels into a composite channel using perfect
* reconstruction filters (-6db at band edge). Output sample rate is the same as the input sample rate of one of
* the channels.
*/
public TwoChannelSynthesizerM2(float[] filter)
{
init(filter);
}
/**
* Initializes the synthesizer filter and data buffers for operation.
*
* @param filter to use for polyphase synthesis of two channels.
*/
private void init(float[] filter)
{
int tapsPerChannel = (int)Math.ceil(filter.length / 2);
mIQInterleavedFilter = getInterleavedFilter(filter, tapsPerChannel);
mSerpentineDataBuffer = new float[mIQInterleavedFilter.length];
mFilterVectorProduct = new float[mIQInterleavedFilter.length];
}
/**
* Synthesizes a new channel from the channel 1 and channel 2 sample arrays that are arranged as
* i0, q0, i1, q1 ... iN-1, qN-1
*
* @param channelBuffer1 input channel
* @param channelBuffer2 input channel
* @return synthesized channel results of the same length as both channel 1/2 input arrays.
*/
public ReusableComplexBuffer process(ReusableComplexBuffer channelBuffer1, ReusableComplexBuffer channelBuffer2)
{
if(channelBuffer1.getSamples().length != channelBuffer1.getSamples().length)
{
throw new IllegalArgumentException("Channel 1 and 2 array length must be equal");
}
float[] channel1 = channelBuffer1.getSamples();
float[] channel2 = channelBuffer2.getSamples();
ReusableComplexBuffer synthesizedComplexBuffer = mReusableComplexBufferQueue.getBuffer(channel1.length);
float[] output = synthesizedComplexBuffer.getSamples();
for(int x = 0; x < channel1.length; x += 2)
{
//Load samples from each channel into buffer for IFFT
mIFFTBuffer[0] = channel1[x]; //i
mIFFTBuffer[1] = channel1[x + 1]; //q
mIFFTBuffer[2] = channel2[x]; //i
mIFFTBuffer[3] = channel2[x + 1]; //q
//Perform Inverse FFT (IFFT)
mFFT.complexInverse(mIFFTBuffer, true);
//Perform serpentine shift of data blocks in the data buffer - make room for 4 new samples
System.arraycopy(mSerpentineDataBuffer, 0, mSerpentineDataBuffer, 4, mSerpentineDataBuffer.length - 4);
//Top Block - load samples into data buffer in normal order
if(mTopBlockFlag)
{
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 0, mIFFTBuffer.length);
}
//Bottom Block - swap samples via data loading to account for phase shift
else
{
System.arraycopy(mIFFTBuffer, 0, mSerpentineDataBuffer, 2, 2);
System.arraycopy(mIFFTBuffer, 2, mSerpentineDataBuffer, 0, 2);
}
//Note: in order to use Java's ability to leverage SIMD intrinsics, we perform filtering in two steps
//(multiply then accumulate) since our filter and data are structured with I and Q vectors interleaved.
//This approach allows the Hotspot compiler to more easily recognize the scalor operations.
//Multiply data samples by the I/Q interleaved filter to form the vector product
for(int y = 0; y < mSerpentineDataBuffer.length; y++)
{
mFilterVectorProduct[y] = mSerpentineDataBuffer[y] * mIQInterleavedFilter[y];
}
//Accumulate output I/Q samples from vector product
mIAccumulator = 0.0f;
mQAccumulator = 0.0f;
for(int y = 0; y < mFilterVectorProduct.length; y += 2)
{
mIAccumulator += mFilterVectorProduct[y];
mQAccumulator += mFilterVectorProduct[y + 1];
}
output[x] = mIAccumulator;
output[x + 1] = mQAccumulator;
mTopBlockFlag = !mTopBlockFlag;
}
channelBuffer1.decrementUserCount();
channelBuffer2.decrementUserCount();
return synthesizedComplexBuffer;
}
/**
* Creates an interleaved I/Q filter where each coefficient from the filter argument is duplicated and the returned
* filter is twice the length of the original filter.
*
* Note: the returned filter array is sized to: 2 * channel count * taps per channel, which may be slightly more
* than twice the length of the original filter. Any Added filter array elements will contain zero values.
*
* @param coefficients to create an interleaved filter
* @param tapsPerChannel count
* @return
*/
private static float[] getInterleavedFilter(float[] coefficients, int tapsPerChannel)
{
int channelCount = 2;
float[] filter = new float[channelCount * tapsPerChannel * 2];
int coefficientPointer = 0;
int filterPointer = 0;
//Create a new filter that duplicates each tap to produce an interleaved I/Q filter
while(coefficientPointer < coefficients.length)
{
filter[filterPointer++] = coefficients[coefficientPointer];
filter[filterPointer++] = coefficients[coefficientPointer++];
}
return filter;
}
}

View File

@@ -0,0 +1,174 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.output;
import io.github.dsheirer.dsp.mixer.IOscillator;
import io.github.dsheirer.dsp.mixer.Oscillator;
import io.github.dsheirer.sample.IOverflowListener;
import io.github.dsheirer.sample.buffer.OverflowableReusableBufferTransferQueue;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.source.Source;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public abstract class ChannelOutputProcessor implements IPolyphaseChannelOutputProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(ChannelOutputProcessor.class);
private OverflowableReusableBufferTransferQueue<ReusableChannelResultsBuffer> mChannelResultsQueue;
private List<ReusableChannelResultsBuffer> mChannelResultsToProcess = new ArrayList<>();
private int mMaxResultsToProcess;
private int mInputChannelCount;
private IOscillator mFrequencyCorrectionMixer;
private boolean mFrequencyCorrectionEnabled;
private double mGain = 1.0;
/**
* Base class for polyphase channelizer output channel processing. Provides built-in frequency translation
* oscillator support to apply frequency correction to the channel sample stream as requested by sample consumer.
*
* @param inputChannelCount is the number of input channels for this output processor
* @param sampleRate of the output channel. This is used to match the oscillator's sample rate to the output
* channel sample rate for frequency translation/correction.
*/
public ChannelOutputProcessor(int inputChannelCount, double sampleRate, double gain)
{
mInputChannelCount = inputChannelCount;
mGain = gain;
//TODO: swap this out and use the LowPhaseNoiseOscillator
mFrequencyCorrectionMixer = new Oscillator(0, sampleRate);
mMaxResultsToProcess = (int)(sampleRate / 10) * 2; //process at 100 millis interval, twice the expected inflow rate
mChannelResultsQueue = new OverflowableReusableBufferTransferQueue<>((int)(sampleRate * 3), (int)(sampleRate * 0.5));
}
protected double getGain()
{
return mGain;
}
@Override
public int getPolyphaseChannelIndexCount()
{
return mInputChannelCount;
}
public void dispose()
{
if(mChannelResultsQueue != null)
{
mChannelResultsQueue.dispose();
mChannelResultsQueue = null;
}
}
/**
* Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
* @param frequencyOffset in hertz
*/
@Override
public void setFrequencyOffset(long frequencyOffset)
{
mFrequencyCorrectionMixer.setFrequency(frequencyOffset);
mFrequencyCorrectionEnabled = (frequencyOffset != 0);
}
/**
* Oscillator/mixer to use for frequency correction against incoming sample stream
*/
protected IOscillator getFrequencyCorrectionMixer()
{
return mFrequencyCorrectionMixer;
}
@Override
public void receiveChannelResults(ReusableChannelResultsBuffer channelResults)
{
mChannelResultsQueue.offer(channelResults);
}
/**
* Processes all enqueued polyphase channelizer results until the internal queue is empty
* @param reusableComplexBufferAssembler to receive the processed channel results
*/
@Override
public void processChannelResults(ReusableComplexBufferAssembler reusableComplexBufferAssembler)
{
try
{
int toProcess = mChannelResultsQueue.drainTo(mChannelResultsToProcess, mMaxResultsToProcess);
if(toProcess > 0)
{
process(mChannelResultsToProcess, reusableComplexBufferAssembler);
}
}
catch(Throwable throwable)
{
mLog.error("Error while processing polyphase channel samples", throwable);
}
mChannelResultsToProcess.clear();
}
/**
* Sub-class implementation to process one polyphase channelizer result array.
* @param channelResults to process
* @param reusableComplexBufferAssembler to receive the processed channelizer results
*/
public abstract void process(List<ReusableChannelResultsBuffer> channelResults,
ReusableComplexBufferAssembler reusableComplexBufferAssembler);
/**
* Sets the overflow listener to monitor the internal channelizer channel results queue overflow state
*/
public void setSourceOverflowListener(Source source)
{
mChannelResultsQueue.setSourceOverflowListener(source);
}
/**
* Removes the overflow listener from monitoring the internal channelizer channel results queue overflow state
*/
public void removeOverflowListener(IOverflowListener listener)
{
mChannelResultsQueue.setOverflowListener(null);
}
@Override
public int getInputChannelCount()
{
return mInputChannelCount;
}
/**
* Indicates if this channel has a frequency correction offset
*/
public boolean hasFrequencyCorrection()
{
return mFrequencyCorrectionEnabled;
}
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.output;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.source.Source;
import java.util.List;
public interface IPolyphaseChannelOutputProcessor
{
/**
* Receive and enqueue output results from the polyphase analysis channelizer
* @param channelResultsBuffer to enqueue
*/
void receiveChannelResults(ReusableChannelResultsBuffer channelResultsBuffer);
/**
* Process the channel output channel results queue and deliver the output to the listener
*
* @param reusableComplexBufferAssembler to receive the processed channel results
*/
void processChannelResults(ReusableComplexBufferAssembler reusableComplexBufferAssembler);
/**
* 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.
*
* @param frequency in hertz
*/
void setFrequencyOffset(long frequency);
/**
* Indicates the number of input channels processed by this output processor
* @return
*/
int getInputChannelCount();
/**
* Updates the input polyphase channel index(es) used by this output processor
* @param indexes
*/
void setPolyphaseChannelIndices(List<Integer> indexes);
/**
* List of current polyphase channel indices for the output processor
*/
int getPolyphaseChannelIndexCount();
/**
* Updates the synthesis filter taps for this output processor
* @param filter
*/
void setSynthesisFilter(float[] filter);
/**
* Sets the listener to receive buffer overflow notifications
* @param source to receive overflow notifications
*/
void setSourceOverflowListener(Source source);
void dispose();
}

View File

@@ -0,0 +1,107 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.output;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
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);
private int mChannelOffset;
/**
* Processor to extract a single channel from a polyphase channelizer and produce an output I/Q complex sample
* from each polyphase channelizer output results array.
*
* @param sampleRate of the output sample stream.
* @param channelIndexes containing a single channel index.
* @param gain value to apply. Typically this is the same as the channelizer's channel count.
*/
public OneChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, double gain)
{
super(1, sampleRate, gain);
setPolyphaseChannelIndices(channelIndexes);
}
@Override
public void setSynthesisFilter(float[] filter)
{
throw new IllegalArgumentException("The one channel output processor does not support filter updates");
}
/**
* Updates this processor to extract a single, specified channel index.
* @param indexes containing a single channel index value.
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
*/
public void setPolyphaseChannelIndices(List<Integer> indexes)
{
if(indexes.size() != 1)
{
throw new IllegalArgumentException("Single channel output processor requires a single index to " +
"process - provided indexes " + indexes.toString());
}
//Set the channelized output results offset to twice the channel index to account for each channel having
//an I/Q pair
mChannelOffset = indexes.get(0) * 2;
}
/**
* 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 channelResults to process containing a list of channel array of I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
* @param reusableComplexBufferAssembler to receive the extracted, frequency-translated channel results
*/
@Override
public void process(List<ReusableChannelResultsBuffer> channelResults,
ReusableComplexBufferAssembler reusableComplexBufferAssembler)
{
for(ReusableChannelResultsBuffer channelResultsBuffer: channelResults)
{
try
{
ReusableComplexBuffer channelBuffer = channelResultsBuffer.getChannel(mChannelOffset);
if(hasFrequencyCorrection())
{
getFrequencyCorrectionMixer().mixComplex(channelBuffer.getSamples());
}
channelBuffer.applyGain(getGain());
reusableComplexBufferAssembler.receive(channelBuffer);
}
catch(IllegalArgumentException iae)
{
mLog.error("Error extracting channel samples from polyphase channel results buffer");
}
channelResultsBuffer.decrementUserCount();
}
}
}

View File

@@ -0,0 +1,187 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.channelizer.output;
import io.github.dsheirer.dsp.filter.channelizer.TwoChannelSynthesizerM2;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.dsp.mixer.FS4DownConverter;
import io.github.dsheirer.sample.buffer.ReusableChannelResultsBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TwoChannelOutputProcessor extends ChannelOutputProcessor
{
private final static Logger mLog = LoggerFactory.getLogger(TwoChannelOutputProcessor.class);
private static Map<Integer,float[]> sLowPassFilterKernelMap = new HashMap<>();
private TwoChannelSynthesizerM2 mSynthesizer;
private FS4DownConverter mFS4DownConverter = new FS4DownConverter();
private ComplexFIRFilter2 mLowPassFilter;
private int mChannelOffset1;
private int mChannelOffset2;
/**
* Processor to extract two channels from a polyphase channelizer, synthesize/recombine the channels, apply
* frequency translation and frequency correction, down-sample by a factor of two and output an I/Q complex sample.
*
* @param sampleRate of the output sample stream.
* @param channelIndexes containing two channel indices.
* @param gain to apply to output. Typically this is equal to the channelizer's channel count.
*/
public TwoChannelOutputProcessor(double sampleRate, List<Integer> channelIndexes, float[] filter, double gain)
{
//Set the frequency correction oscillator to 2 x output sample rate since we'll be correcting the frequency
//after synthesizing both input channels
super(2, sampleRate, gain);
setPolyphaseChannelIndices(channelIndexes);
setSynthesisFilter(filter);
float[] lowPassFilter = sLowPassFilterKernelMap.get((int)sampleRate);
if(lowPassFilter == null)
{
try
{
lowPassFilter = getLowPassFilter(sampleRate);
sLowPassFilterKernelMap.put((int)sampleRate, lowPassFilter);
}
catch(FilterDesignException fde)
{
mLog.error("Couldn't design a low-pass filter for 2-channel synthesizer", fde);
throw new IllegalStateException("Cannot design low-pass filter for 2 channel synthesizer", fde);
}
}
mLowPassFilter = new ComplexFIRFilter2(lowPassFilter, 1.0f);
}
/**
* Sets the frequency offset to apply to the incoming samples to mix the desired signal to baseband.
* @param frequencyOffset in hertz
*/
@Override
public void setFrequencyOffset(long frequencyOffset)
{
super.setFrequencyOffset(frequencyOffset);
}
@Override
public void setSynthesisFilter(float[] filter)
{
mSynthesizer = new TwoChannelSynthesizerM2(filter);
}
/**
* Updates this processor to extract the two specified channel indexes.
*
* @param indexes containing a single channel index value.
* @throws IllegalArgumentException if the list of indexes does not contain a single channel index.
*/
public void setPolyphaseChannelIndices(List<Integer> indexes)
{
if(indexes.size() != 2)
{
throw new IllegalArgumentException("Double channel output processor requires two indexes to " +
"process - provided indexes " + indexes.toString());
}
//Set the channelized output results offsets to twice the channel index to account for each channel having
//an I/Q pair
mChannelOffset1 = indexes.get(0) * 2;
mChannelOffset2 = indexes.get(1) * 2;
}
/**
* 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 channelResultsBuffers to process containing an array of channel I/Q sample pairs (I0,Q0,I1,Q1...In,Qn)
* @param reusableComplexBufferAssembler to receive the extracted, frequency-translated channel results
*/
@Override
public void process(List<ReusableChannelResultsBuffer> channelResultsBuffers,
ReusableComplexBufferAssembler reusableComplexBufferAssembler)
{
for(ReusableChannelResultsBuffer buffer: channelResultsBuffers)
{
ReusableComplexBuffer channel1 = buffer.getChannel(mChannelOffset1);
ReusableComplexBuffer channel2 = buffer.getChannel(mChannelOffset2);
//Join the two channels using the synthesizer
ReusableComplexBuffer synthesized = mSynthesizer.process(channel1, channel2);
//The synthesized channels are centered at +FS/4 ... downconvert to center the spectrum
mFS4DownConverter.mixComplex(synthesized.getSamples());
//Apply offset and frequency correction to center the signal of interest within the synthesized channel
getFrequencyCorrectionMixer().mixComplex(synthesized.getSamples());
ReusableComplexBuffer lowPassFiltered = mLowPassFilter.filter(synthesized);
lowPassFiltered.applyGain(getGain());
reusableComplexBufferAssembler.receive(lowPassFiltered);
buffer.decrementUserCount();
}
}
/**
* Creates a low-pass filter kernel for the specified sample rate.
* @param sampleRate for the channel
* @return filter
* @throws FilterDesignException if the filter cannot be designed
*/
private static float[] getLowPassFilter(double sampleRate) throws FilterDesignException
{
double cutoff = sampleRate / 4.0;
int passBandCutoff = (int)(cutoff * 0.92);
int stopBandStart = (int)(cutoff * 1.08);
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(sampleRate)
.gridDensity(16)
.oddLength(true)
.passBandCutoff((int)passBandCutoff)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart((int)stopBandStart)
.stopBandAmplitude(0.0)
.stopBandRipple(0.005) //Approximately 94 dB attenuation
.build();
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
if(designer.isValid())
{
return designer.getImpulseResponse();
}
throw new FilterDesignException("Couldn't design low-pass filter for sample rate: " + sampleRate +
" Pass End:" + passBandCutoff + " Stop Begin:" + stopBandStart);
}
}

View File

@@ -1,82 +1,119 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.cic;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.Window;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter_CB_CB;
import io.github.dsheirer.dsp.filter.halfband.complex.HalfBandFilter_CB_CB;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.complex.ComplexFIRFilter2;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferAssembler;
import io.github.dsheirer.sample.complex.ComplexSampleListener;
import io.github.dsheirer.sample.complex.ComplexToComplexBufferAssembler;
import io.github.dsheirer.sample.decimator.ComplexDecimator;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
/**
* Non-Recursive Prime-Factor CIC Filter.
*
* Implements the CIC filter described in Understanding Digital Signal Processing, 3e, Lyons, on page 769. This
* filter is comprised of multiple decimating stages each with a prime factor decimation rate. Multiple stages are
* cascaded to achieve the overall decimation rate.
*
* This filter currently supports a maximum decimation rate of 2801. Higher decimation rates can be added by
* adding additional prime factors to the PRIMES array.
*/
public class ComplexPrimeCICDecimate implements Listener<ReusableComplexBuffer>
{
// private final static Logger mLog = LoggerFactory.getLogger(ComplexPrimeCICDecimate.class);
/**
* PRIME numbers to use for decimation stage sizing. This list contains values that should
* support input sample rates up to 70 MHz with an output channel rate of at least 25 kHz.
*/
public static int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53,
59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251,
257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359,
367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691};
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701,
709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827,
829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953,
967, 971, 977, 983, 991, 997, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051,
1061, 1063, 1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163,
1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259, 1277, 1279,
1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361, 1367, 1373, 1381, 1399,
1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453, 1459, 1471, 1481, 1483, 1487, 1489,
1493, 1499, 1511, 1523, 1531, 1543, 1549, 1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601,
1607, 1609, 1613, 1619, 1621, 1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709,
1721, 1723, 1733, 1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831,
1847, 1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949, 1951,
1973, 1979, 1987, 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039, 2053, 2063, 2069,
2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, 2131, 2137, 2141, 2143, 2153, 2161, 2179,
2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251, 2267, 2269, 2273, 2281, 2287, 2293, 2297,
2309, 2311, 2333, 2339, 2341, 2347, 2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399,
2411, 2417, 2423, 2437, 2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543,
2549, 2551, 2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671,
2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741, 2749, 2753,
2767, 2777, 2789, 2791, 2797, 2801
};
private ArrayList<DecimatingStage> mDecimatingStages =
new ArrayList<DecimatingStage>();
private static Map<Integer,float[]> sLowPassFilters = new HashMap();
private static Map<Integer,List<Integer>> sPrimeFactors = new HashMap();
private List<DecimatingStage> mDecimatingStages = new ArrayList<DecimatingStage>();
private DecimatingStage mFirstDecimatingStage;
private Output mOutput;
/**
* Non-Recursive Prime-Factor CIC Filter with float sample array inputs and
* decimated, single paired i/q float sample output.
* Constructs a new decimation filter.
*
* Implements the CIC filter described in Understanding Digital Signal
* Processing, 3e, Lyons, on page 769. This filter is comprised of multiple
* decimating stages each with a prime factor decimation rate. Multiple
* stages are cascaded to achieve the overall decimation rate.
* An example setup for a 2.4 MHz input sample rate and 25 kHz output channel rate with a desired 12.5 kHz channel
* pass band might be:
* sampleRate = 2,400,000.0
* decimation = 96
* passFrequency = 6250 (1/4 of the channel rate)
* stopFrequency = 7000
*
* This filter supports a maximum decimation rate of 700. This filter can
* be adapted to higher decimation rates by adding additional prime factors
* to the PRIMES array.
* Note: the transition bandwidth (stop - start) directly impacts the size of the low-pass cleanup filter,
* where smaller transition bandwidth creates a longer filter that requires more processing effort.
*
* @param sampleRate of the input sample stream
* @param decimation - overall decimation rate
* @param order - filter order
* @param passFrequency for the final cleanup filter
* @param stopFrequency for the final cleanup filter
* @throws FilterDesignException if a final low-pass cleanup filter cannot be created for the output channel rate
* and specified pass/stop frequencies.
*/
public ComplexPrimeCICDecimate(int decimation, int order,
int passFrequency, int attenuation, Window.WindowType windowType)
public ComplexPrimeCICDecimate(double sampleRate, int decimation, double passFrequency, double stopFrequency)
throws FilterDesignException
{
Validate.isTrue(decimation <= 700);
Validate.isTrue(decimation <= PRIMES[PRIMES.length - 1]);
List<Integer> stageSizes = getPrimeFactors(decimation);
for(int x = 0; x < stageSizes.size(); x++)
{
DecimatingStage stage = new DecimatingStage(stageSizes.get(x), order);
DecimatingStage stage = new DecimatingStage(stageSizes.get(x), 1);
mDecimatingStages.add(stage);
@@ -87,15 +124,16 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
}
else
{
/* Wire the current stage to the previous stage */
/* Wire the current stage to the previous stage */
mDecimatingStages.get(x - 1).setListener(stage);
}
}
mOutput = new Output(48000, passFrequency, attenuation, windowType);
double channelRate = sampleRate / (double)decimation;
mDecimatingStages.get(mDecimatingStages.size() - 1)
.setListener(mOutput);
mOutput = new Output(channelRate, passFrequency, stopFrequency);
mDecimatingStages.get(mDecimatingStages.size() - 1).setListener(mOutput);
}
public void dispose()
@@ -116,7 +154,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
/**
* Adds a listener to receive the output of this CIC decimation filter
*/
public void setListener(Listener<ComplexBuffer> listener)
public void setListener(Listener<ReusableComplexBuffer> listener)
{
mOutput.setListener(listener);
}
@@ -130,16 +168,26 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
}
/**
* Calculates the prime factors of the decimation rate up to a maximum
* decimation rate of 700. If you wish to have decimation rates higher
* than 700, then add additional prime factors to the PRIMES array.
* Calculates the prime factors of the decimation rate. If you wish to have decimation rates higher than the
* highest rate listed in the PRIMES array, then add additional prime factors to the PRIMES array.
*
* @param decimation - integral decimation rate
* @return - ordered list (smallest to largest) of prime factors
*/
public static List<Integer> getPrimeFactors(int decimation)
{
ArrayList<Integer> stages = new ArrayList<Integer>();
if(decimation > PRIMES[PRIMES.length - 1])
{
throw new IllegalArgumentException("Decimation rates higher than [" + PRIMES[PRIMES.length - 1] +
"] are not supported. Update the PRIMES array to add support for higher decimation rates");
}
if(sPrimeFactors.containsKey(decimation))
{
return sPrimeFactors.get(decimation);
}
List<Integer> primeFactors = new ArrayList<Integer>();
int pointer = 0;
@@ -149,8 +197,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
if(decimation % prime == 0)
{
stages.add(prime);
primeFactors.add(prime);
decimation /= prime;
}
else
@@ -159,14 +206,16 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
}
}
return stages;
sPrimeFactors.put(decimation, primeFactors);
return primeFactors;
}
/**
* Primary input method for receiving sample arrays composed as I,Q,I,Q, etc.
*/
@Override
public void receive(ComplexBuffer buffer)
public void receive(ReusableComplexBuffer buffer)
{
if(mFirstDecimatingStage != null)
{
@@ -177,6 +226,8 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
mFirstDecimatingStage.receive(samples[x], samples[x + 1]);
}
}
buffer.decrementUserCount();
}
/**
@@ -186,7 +237,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
*/
public class DecimatingStage implements ComplexSampleListener
{
private ArrayList<Stage> mStages = new ArrayList<Stage>();
private List<Stage> mStages = new ArrayList<Stage>();
private Stage mFirstStage;
private ComplexDecimator mDecimator;
@@ -218,7 +269,6 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
}
mDecimator = new ComplexDecimator(size);
mStages.get(mStages.size() - 1).setListener(mDecimator);
}
@@ -249,9 +299,8 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
}
/**
* Single non-decimating CIC stage component. Uses a circular buffer and a
* running average internally to implement the stage so that stage size has
* essentially no impact on the computational requirements of the stage
* Single non-decimating CIC stage component. Uses a circular buffer and a running average internally to implement
* the stage so that stage size has essentially no impact on the computational requirements of the stage
*/
public class Stage implements ComplexSampleListener
{
@@ -268,7 +317,7 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
protected float mGain;
public Stage()
protected Stage()
{
}
@@ -279,7 +328,8 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
mISamples = new float[mSize];
mQSamples = new float[mSize];
mGain = 1.0f / (float)size;
float baseGain = 1.0f / (float)size;
mGain = baseGain * (1.0f / (1.0f - baseGain));
}
public void dispose()
@@ -289,11 +339,11 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
public void receive(float i, float q)
{
/* Subtract the oldest sample and add back in the newest */
/* Subtract the oldest sample and add in the newest sample */
mISum = mISum - mISamples[mSamplePointer] + i;
mQSum = mQSum - mQSamples[mSamplePointer] + q;
/* Overwrite the oldest sample with the newest */
/* Overwrite the oldest sample with the newest */
mISamples[mSamplePointer] = i;
mQSamples[mSamplePointer] = q;
@@ -317,30 +367,24 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
}
/**
* Size 2 stage that removes the unnecessary circular buffer management for
* a two-stage.
* Size 2 stage that eliminates the unnecessary circular buffer management for a two-stage.
*/
public class TwoStage extends Stage
{
public TwoStage()
{
super();
mGain = 0.5f;
}
public void receive(float i, float q)
{
float iSum = mISum + i;
float qSum = mQSum + q;
if(mListener != null)
{
mListener.receive(((mISum + i) * mGain), ((mQSum + q) * mGain));
}
mISum = i;
mQSum = q;
if(mListener != null)
{
mListener.receive((iSum * mGain), (qSum * mGain));
}
}
}
@@ -351,62 +395,99 @@ public class ComplexPrimeCICDecimate implements Listener<ComplexBuffer>
*/
public class Output implements ComplexSampleListener
{
/* Decimated output buffers will contain 1024 samples */
private ComplexToComplexBufferAssembler mAssembler =
new ComplexToComplexBufferAssembler(2048);
/* Decimated output buffers will contain 1024 complex samples */
private ReusableComplexBufferAssembler mBufferAssembler;
private ComplexFIRFilter2 mLowPassFilter;
private Listener<ReusableComplexBuffer> mReusableComplexBufferListener;
private ComplexFIRFilter_CB_CB mCleanupFilter;
private HalfBandFilter_CB_CB mHalfBandFilter = new HalfBandFilter_CB_CB(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false);
public Output(int outputSampleRate, int passFrequency, int attenuation,
Window.WindowType windowType)
public Output(double outputSampleRate, double passFrequency, double stopFrequency) throws FilterDesignException
{
mCleanupFilter = new ComplexFIRFilter_CB_CB(FilterFactory
.getCICCleanupFilter(outputSampleRate,
passFrequency,
attenuation,
windowType), 0.4f);
mBufferAssembler = new ReusableComplexBufferAssembler(2400, outputSampleRate);
//Bypassing the CIC cleanup filter for now
mAssembler.setListener(mHalfBandFilter);
//This may throw an exception if we can't design a filter for the sample rate and pass/stop frequencies
float[] filterCoefficients = getLowPassFilter(outputSampleRate, passFrequency, stopFrequency);
mLowPassFilter = new ComplexFIRFilter2(filterCoefficients, 1.0f);
// mAssembler.setListener( mCleanupFilter );
// mCleanupFilter.setListener( mHalfBandFilter );
mBufferAssembler.setListener(new Listener<ReusableComplexBuffer>()
{
@Override
public void receive(ReusableComplexBuffer reusableComplexBuffer)
{
if(mReusableComplexBufferListener != null)
{
ReusableComplexBuffer filteredBuffer = mLowPassFilter.filter(reusableComplexBuffer);
mReusableComplexBufferListener.receive(filteredBuffer);
// mReusableComplexBufferListener.receive(reusableComplexBuffer);
}
else
{
reusableComplexBuffer.decrementUserCount();
}
}
});
}
public void dispose()
{
mAssembler.dispose();
mCleanupFilter.dispose();
mHalfBandFilter.dispose();
mBufferAssembler.dispose();
mLowPassFilter.dispose();
}
/**
* Receiver method for the output adapter to receive a filtered,
* decimated sample, apply gain correction, apply cleanup filtering
* and output the sample values as a complex sample.
* Interface for receiving CIC decimated output samples
*/
@Override
public void receive(float inphase, float quadrature)
{
mAssembler.receive(inphase, quadrature);
mBufferAssembler.receive(inphase, quadrature);
}
/**
* Adds a listener to receive output samples
*/
public void setListener(Listener<ComplexBuffer> listener)
public void setListener(Listener<ReusableComplexBuffer> listener)
{
mHalfBandFilter.setListener(listener);
mReusableComplexBufferListener = listener;
}
/**
* Removes the listener from receiving output samples
*/
public void removeListener()
{
mHalfBandFilter.removeListener();
mReusableComplexBufferListener = null;
}
/**
* Creates a low-pass filter to use as the final cleanup filter for the decimated output stream
*
* @param sampleRate for the final output channel rate
* @param passFrequency for half of the desired channel rate
* @param stopFrequency for the attenuated band
* @return a newly designed filter or a previously designed (cached) filter
* @throws FilterDesignException
*/
private float[] getLowPassFilter(double sampleRate, double passFrequency, double stopFrequency) throws FilterDesignException
{
//Use existing filter if we've already designed one
if(sLowPassFilters.containsKey((int)sampleRate))
{
return sLowPassFilters.get((int)sampleRate);
}
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(sampleRate)
.gridDensity(16)
.passBandCutoff(passFrequency)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart(stopFrequency)
.stopBandAmplitude(0.0)
.stopBandRipple(0.01)
.build();
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
//This will throw an exception if the filter cannot be designed
float[] taps = designer.getImpulseResponse();
sLowPassFilters.put((int)sampleRate, taps);
return taps;
}
}
}

View File

@@ -1,17 +1,17 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2015 Dennis Sheirer
*
*
* This 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/>
******************************************************************************/
@@ -20,13 +20,13 @@ package io.github.dsheirer.dsp.filter.cic;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.Filters;
import io.github.dsheirer.dsp.filter.Window.WindowType;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter_RB_RB;
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter_RB_RB;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter2;
import io.github.dsheirer.dsp.filter.halfband.real.HalfBandFilter;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.buffer.ReusableBufferAssembler;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import io.github.dsheirer.sample.decimator.RealDecimator;
import io.github.dsheirer.sample.real.RealBuffer;
import io.github.dsheirer.sample.real.RealSampleListener;
import io.github.dsheirer.sample.real.RealToRealBufferAssembler;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,381 +34,389 @@ import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
public class RealPrimeCICDecimate
public class RealPrimeCICDecimate
{
private final static Logger mLog =
LoggerFactory.getLogger( RealPrimeCICDecimate.class );
public static int[] PRIMES = { 2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,
59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,
157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,
257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,
367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,
467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,
599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691 };
private ArrayList<DecimatingStage> mDecimatingStages =
new ArrayList<DecimatingStage>();
private DecimatingStage mFirstDecimatingStage;
private final static Logger mLog = LoggerFactory.getLogger(RealPrimeCICDecimate.class);
private Output mOutput;
private int mOutputBufferSize = 2048;
public static int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53,
59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151,
157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251,
257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359,
367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593,
599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691};
/**
* Non-Recursive Prime-Factor CIC Filter with float sample array inputs and
* decimated float sample output.
*
* Implements the CIC filter described in Understanding Digital Signal
* Processing, 3e, Lyons, on page 769. This filter is comprised of multiple
* decimating stages each with a prime factor decimation rate. Multiple
* stages are cascaded to achieve the overall decimation rate.
*
* This filter supports a maximum decimation rate of 700. This filter can
* be adapted to higher decimation rates by adding additional prime factors
* to the PRIMES array.
*
* @param decimation - overall decimation rate
* @param order - filter order
*/
public RealPrimeCICDecimate( int decimation, int order, int passFrequency,
int attenuation, WindowType windowType )
{
this( decimation, order, passFrequency, attenuation, windowType, 2048 );
}
public RealPrimeCICDecimate( int decimation, int order, int passFrequency,
int attenuation, WindowType windowType, int outputBufferSize )
{
Validate.isTrue(decimation <= 700);
private List<DecimatingStage> mDecimatingStages = new ArrayList<DecimatingStage>();
private DecimatingStage mFirstDecimatingStage;
private Output mOutput;
mOutputBufferSize = outputBufferSize;
private int mOutputBufferSize = 2048;
List<Integer> stageSizes = getPrimeFactors( decimation );
for( int x = 0; x < stageSizes.size(); x++ )
{
DecimatingStage stage = new DecimatingStage( stageSizes.get( x ), order );
mDecimatingStages.add( stage );
if( x == 0 )
{
/* Reference to first stage -- will receive all samples */
mFirstDecimatingStage = stage;
}
else
{
/* Wire the current stage to the previous stage */
mDecimatingStages.get( x - 1 ).setListener( stage );
}
}
mOutput = new Output( 48000, passFrequency, attenuation, windowType,
outputBufferSize );
mDecimatingStages.get( mDecimatingStages.size() - 1 )
.setListener( mOutput );
}
public void dispose()
{
for( DecimatingStage stage: mDecimatingStages )
{
stage.dispose();
}
mDecimatingStages.clear();
mDecimatingStages = null;
mFirstDecimatingStage = null;
mOutput.dispose();
mOutput = null;
}
/**
* Adds a listener to receive the output of this CIC decimation filter
*/
public void setListener( Listener<RealBuffer> listener )
{
mOutput.setListener( listener );
}
/**
* Removes listener from output of this CIC decimation filter
*/
public void removeListener()
{
mOutput.removeListener();
}
/**
* Calculates the prime factors of the decimation rate up to a maximum
* decimation rate of 700. If you wish to have decimation rates higher
* than 700, then add additional prime factors to the PRIMES array.
*
* @param decimation - integral decimation rate
* @return - ordered list (smallest to largest) of prime factors
*/
public static List<Integer> getPrimeFactors( int decimation )
{
ArrayList<Integer> stages = new ArrayList<Integer>();
int pointer = 0;
while( decimation > 0 && pointer < PRIMES.length )
{
int prime = PRIMES[ pointer ];
if( decimation % prime == 0 )
{
stages.add( prime );
decimation /= prime;
}
else
{
pointer++;
}
}
return stages;
}
/**
* Primary input method for receiving real buffers.
*/
public void receive( RealBuffer buffer )
/**
* Non-Recursive Prime-Factor CIC Filter with float sample array inputs and
* decimated float sample output.
*
* Implements the CIC filter described in Understanding Digital Signal
* Processing, 3e, Lyons, on page 769. This filter is comprised of multiple
* decimating stages each with a prime factor decimation rate. Multiple
* stages are cascaded to achieve the overall decimation rate.
*
* This filter supports a maximum decimation rate of 700. This filter can
* be adapted to higher decimation rates by adding additional prime factors
* to the PRIMES array.
*
* @param decimation - overall decimation rate
* @param order - filter order
*/
public RealPrimeCICDecimate(int decimation, int order, int passFrequency, int attenuation, WindowType windowType)
{
if( mFirstDecimatingStage != null )
{
for( float sample: buffer.getSamples() )
{
mFirstDecimatingStage.receive( sample );
}
}
this(decimation, order, passFrequency, attenuation, windowType, 2048);
}
/**
* Decimating stage combines multiple CIC stages with a decimator. The
* number of stages is indicated by the order value and the size indicates
* the decimation rate of this stage.
*/
public class DecimatingStage implements RealSampleListener
{
private ArrayList<Stage> mStages = new ArrayList<Stage>();
private Stage mFirstStage;
private RealDecimator mDecimator;
public DecimatingStage( int size, int order )
{
for( int x = 0; x < order; x++ )
{
Stage stage;
if( size == 2 )
{
stage = new TwoStage();
}
else
{
stage = new Stage( size );
}
mStages.add( stage );
if( x == 0 )
{
mFirstStage = stage;
}
else
{
mStages.get( x - 1 ).setListener( stage );
}
}
mDecimator = new RealDecimator( size );
mStages.get( mStages.size() - 1 ).setListener( mDecimator );
}
public void dispose()
{
for( Stage stage: mStages )
{
stage.dispose();
}
mStages.clear();
mDecimator.dispose();
mDecimator = null;
mFirstStage = null;
mStages = null;
}
@Override
public void receive( float sample )
public RealPrimeCICDecimate(int decimation, int order, int passFrequency, int attenuation, WindowType windowType, int outputBufferSize)
{
Validate.isTrue(decimation <= 700);
mOutputBufferSize = outputBufferSize;
List<Integer> stageSizes = getPrimeFactors(decimation);
for(int x = 0; x < stageSizes.size(); x++)
{
mFirstStage.receive( sample );
}
public void setListener( RealSampleListener listener )
{
mDecimator.setListener( listener );
}
}
/**
* Single non-decimating CIC stage component. Uses a circular buffer and a
* running average internally to implement the stage so that stage size has
* essentially no impact on the computational requirements of the stage
*/
public class Stage implements RealSampleListener
{
protected RealSampleListener mListener;
private float[] mSamples;
protected float mSum;
private int mSamplePointer = 0;
private int mSize;
protected float mGain;
public Stage()
{
}
DecimatingStage stage = new DecimatingStage(stageSizes.get(x), order);
public Stage( int size )
{
mSize = size - 1;
mSamples = new float[ mSize ];
mGain = 1.0f / (float)size;
}
public void dispose()
{
mListener = null;
}
public void receive( float sample )
{
/* Subtract the oldest sample and add back in the newest */
mSum = mSum - mSamples[ mSamplePointer ] + sample;
mDecimatingStages.add(stage);
/* Overwrite the oldest sample with the newest */
mSamples[ mSamplePointer ] = sample;
mSamplePointer++;
if( mSamplePointer >= mSize )
{
mSamplePointer = 0;
}
if( mListener != null )
{
mListener.receive( mSum * mGain );
}
}
public void setListener( RealSampleListener listener )
{
mListener = listener;
}
}
/**
* Size 2 stage that removes the unnecessary circular buffer management for
* a two-stage.
*/
public class TwoStage extends Stage
{
public TwoStage()
{
super();
mGain = 0.5f;
}
public void receive( float sample )
{
float sum = mSum + sample;
mSum = sample;
if( mListener != null )
{
mListener.receive( sum * mGain );
}
}
}
/**
* Output adapter - applies gain correction, applies cleanup filter, casts
* double-valued samples into float-valued complex sample and sends the
* result to the registered listener.
*/
public class Output implements RealSampleListener
{
/* Decimated output buffers will contain 1024 samples */
private RealToRealBufferAssembler mAssembler;
private RealFIRFilter_RB_RB mCleanupFilter;
private HalfBandFilter_RB_RB mHalfBandFilter = new HalfBandFilter_RB_RB(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false );
public Output( int outputSampleRate, int passFrequency, int attenuation,
WindowType windowType, int outputBufferSize )
{
mCleanupFilter = new RealFIRFilter_RB_RB( FilterFactory
.getCICCleanupFilter( outputSampleRate,
passFrequency,
attenuation,
windowType ), 0.4f );
mAssembler = new RealToRealBufferAssembler( outputBufferSize );
mAssembler.setListener( mCleanupFilter );
mCleanupFilter.setListener( mHalfBandFilter );
}
public void dispose()
{
mAssembler.dispose();
mCleanupFilter.dispose();
mHalfBandFilter.dispose();
}
/**
* Receiver method for the output adapter to receive a filtered,
* decimated sample, apply gain correction, apply cleanup filtering
* and output the sample values as a complex sample.
*/
@Override
public void receive( float sample )
{
mAssembler.receive( sample * 32.0f );
}
/**
* Adds a listener to receive output samples
*/
public void setListener( Listener<RealBuffer> listener )
{
mHalfBandFilter.setListener( listener );
if(x == 0)
{
/* Reference to first stage -- will receive all samples */
mFirstDecimatingStage = stage;
}
else
{
/* Wire the current stage to the previous stage */
mDecimatingStages.get(x - 1).setListener(stage);
}
}
/**
* Removes the listener from receiving output samples
*/
mOutput = new Output(48000, passFrequency, attenuation, windowType,
outputBufferSize);
mDecimatingStages.get(mDecimatingStages.size() - 1)
.setListener(mOutput);
}
public void dispose()
{
for(DecimatingStage stage : mDecimatingStages)
{
stage.dispose();
}
mDecimatingStages.clear();
mDecimatingStages = null;
mFirstDecimatingStage = null;
mOutput.dispose();
mOutput = null;
}
/**
* Adds a listener to receive the output of this CIC decimation filter
*/
public void setListener(Listener<ReusableFloatBuffer> listener)
{
mOutput.setListener(listener);
}
/**
* Removes listener from output of this CIC decimation filter
*/
public void removeListener()
{
mOutput.removeListener();
}
/**
* Calculates the prime factors of the decimation rate up to a maximum
* decimation rate of 700. If you wish to have decimation rates higher
* than 700, then add additional prime factors to the PRIMES array.
*
* @param decimation - integral decimation rate
* @return - ordered list (smallest to largest) of prime factors
*/
public static List<Integer> getPrimeFactors(int decimation)
{
ArrayList<Integer> stages = new ArrayList<Integer>();
int pointer = 0;
while(decimation > 0 && pointer < PRIMES.length)
{
int prime = PRIMES[pointer];
if(decimation % prime == 0)
{
stages.add(prime);
decimation /= prime;
}
else
{
pointer++;
}
}
return stages;
}
/**
* Primary input method for receiving real buffers.
*/
public void receive(ReusableFloatBuffer buffer)
{
if(mFirstDecimatingStage != null)
{
for(float sample : buffer.getSamples())
{
mFirstDecimatingStage.receive(sample);
}
}
buffer.decrementUserCount();
}
/**
* Decimating stage combines multiple CIC stages with a decimator. The
* number of stages is indicated by the order value and the size indicates
* the decimation rate of this stage.
*/
public class DecimatingStage implements RealSampleListener
{
private ArrayList<Stage> mStages = new ArrayList<Stage>();
private Stage mFirstStage;
private RealDecimator mDecimator;
public DecimatingStage(int size, int order)
{
for(int x = 0; x < order; x++)
{
Stage stage;
if(size == 2)
{
stage = new TwoStage();
}
else
{
stage = new Stage(size);
}
mStages.add(stage);
if(x == 0)
{
mFirstStage = stage;
}
else
{
mStages.get(x - 1).setListener(stage);
}
}
mDecimator = new RealDecimator(size);
mStages.get(mStages.size() - 1).setListener(mDecimator);
}
public void dispose()
{
for(Stage stage : mStages)
{
stage.dispose();
}
mStages.clear();
mDecimator.dispose();
mDecimator = null;
mFirstStage = null;
mStages = null;
}
@Override
public void receive(float sample)
{
mFirstStage.receive(sample);
}
public void setListener(RealSampleListener listener)
{
mDecimator.setListener(listener);
}
}
/**
* Single non-decimating CIC stage component. Uses a circular buffer and a
* running average internally to implement the stage so that stage size has
* essentially no impact on the computational requirements of the stage
*/
public class Stage implements RealSampleListener
{
protected RealSampleListener mListener;
private float[] mSamples;
protected float mSum;
private int mSamplePointer = 0;
private int mSize;
protected float mGain;
public Stage()
{
}
public Stage(int size)
{
mSize = size - 1;
mSamples = new float[mSize];
mGain = 1.0f / (float)size;
}
public void dispose()
{
mListener = null;
}
public void receive(float sample)
{
/* Subtract the oldest sample and add back in the newest */
mSum = mSum - mSamples[mSamplePointer] + sample;
/* Overwrite the oldest sample with the newest */
mSamples[mSamplePointer] = sample;
mSamplePointer++;
if(mSamplePointer >= mSize)
{
mSamplePointer = 0;
}
if(mListener != null)
{
mListener.receive(mSum * mGain);
}
}
public void setListener(RealSampleListener listener)
{
mListener = listener;
}
}
/**
* Size 2 stage that removes the unnecessary circular buffer management for
* a two-stage.
*/
public class TwoStage extends Stage
{
public TwoStage()
{
super();
mGain = 0.5f;
}
public void receive(float sample)
{
float sum = mSum + sample;
mSum = sample;
if(mListener != null)
{
mListener.receive(sum * mGain);
}
}
}
/**
* Output adapter - applies gain correction, applies cleanup filter, casts
* double-valued samples into float-valued complex sample and sends the
* result to the registered listener.
*/
public class Output implements RealSampleListener
{
/* Decimated output buffers will contain 1024 samples */
private ReusableBufferAssembler mAssembler;
private Listener<ReusableFloatBuffer> mReusableBufferListener;
private RealFIRFilter2 mCleanupFilter;
private HalfBandFilter mHalfBandFilter = new HalfBandFilter(
Filters.FIR_HALF_BAND_31T_ONE_EIGHTH_FCO.getCoefficients(), 0.4f, false);
public Output(int outputSampleRate, int passFrequency, int attenuation, WindowType windowType, int outputBufferSize)
{
mCleanupFilter = new RealFIRFilter2(FilterFactory.getCICCleanupFilter(outputSampleRate, passFrequency,
attenuation, windowType), 0.4f);
mAssembler = new ReusableBufferAssembler(outputBufferSize);
mAssembler.setListener(new Listener<ReusableFloatBuffer>()
{
@Override
public void receive(ReusableFloatBuffer reusableFloatBuffer)
{
if(mReusableBufferListener != null)
{
ReusableFloatBuffer cleanupFiltered = mCleanupFilter.filter(reusableFloatBuffer);
ReusableFloatBuffer halfBandFiltered = mHalfBandFilter.filter(cleanupFiltered);
mReusableBufferListener.receive(halfBandFiltered);
}
else
{
reusableFloatBuffer.decrementUserCount();
}
}
});
}
public void dispose()
{
mAssembler.dispose();
mCleanupFilter.dispose();
mHalfBandFilter.dispose();
}
/**
* Receiver method for the output adapter to receive a filtered,
* decimated sample, apply gain correction, apply cleanup filtering
* and output the sample values as a complex sample.
*/
@Override
public void receive(float sample)
{
mAssembler.receive(sample * 32.0f);
}
/**
* Adds a listener to receive output samples
*/
public void setListener(Listener<ReusableFloatBuffer> listener)
{
mReusableBufferListener = listener;
}
/**
* Removes the listener from receiving output samples
*/
public void removeListener()
{
mHalfBandFilter.removeListener();
mReusableBufferListener = null;
}
}
}
}

View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2017 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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.correction;
import io.github.dsheirer.sample.buffer.ReusableComplexBuffer;
import io.github.dsheirer.sample.buffer.ReusableComplexBufferQueue;
public class IQCorrectionFilter
{
private float mRatio = 0.00001f;
private float mAverageInphase = 0.0f;
private float mAverageQuadrature = 0.0f;
private ReusableComplexBufferQueue mReusableComplexBufferQueue = new ReusableComplexBufferQueue("IQCorrectionFilter");
public IQCorrectionFilter(float ratio)
{
mRatio = ratio;
}
public IQCorrectionFilter()
{
}
public ReusableComplexBuffer filter(ReusableComplexBuffer buffer)
{
float[] samples = buffer.getSamples();
ReusableComplexBuffer filtered = mReusableComplexBufferQueue.getBuffer(samples.length);
float[] filteredSamples = filtered.getSamples();
for(int x = 0; x < samples.length; x += 2)
{
mAverageInphase = mAverageInphase + (mRatio * (samples[x] - mAverageInphase));
filteredSamples[x] = samples[x] - mAverageInphase;
mAverageQuadrature = mAverageQuadrature + (mRatio * (samples[x + 1] - mAverageQuadrature));
filteredSamples[x + 1] = samples[x + 1] - mAverageQuadrature;
}
return filtered;
}
}

View File

@@ -0,0 +1,98 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class BufferDCFilter
{
private final static Logger mLog = LoggerFactory.getLogger(BufferDCFilter.class);
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("Buffer DC Filter");
private double mAccumulator;
/**
* Calculates the DC offset present in the samples and subtracts the offset from all samples.
*/
public void filterReal(float[] samples)
{
float adjustment = getOffset(samples);
for(int x = 0; x < samples.length; x++)
{
samples[x] = samples[x] - adjustment;
}
}
private float getOffset(float[] samples)
{
mAccumulator = 0.0;
for(int x = 0; x < samples.length; x++)
{
mAccumulator += samples[x];
}
return (float)(mAccumulator / (double)samples.length);
}
/**
* Calculates the DC offset present in the samples and subtracts the offset from all samples.
*/
public static void filterComplex(float[] samples)
{
double iAccumulator = 0.0;
double qAccumulator = 0.0;
for(int x = 0; x < samples.length; x += 2)
{
iAccumulator += samples[x];
qAccumulator += samples[x + 1];
}
double half = (double)samples.length / 2.0;
float iAdjustment = (float)(iAccumulator / half);
float qAdjustment = (float)(qAccumulator / half);
for(int x = 0; x < samples.length; x += 2)
{
samples[x] = samples[x] - iAdjustment;
samples[x + 1] = samples[x + 1] - qAdjustment;
}
}
public ReusableFloatBuffer filter(ReusableFloatBuffer unfilteredBuffer)
{
float[] unfilteredSamples = unfilteredBuffer.getSamples();
ReusableFloatBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredSamples.length);
float[] filteredSamples = filteredBuffer.getSamples();
float offset = getOffset(unfilteredSamples);
for(int x = 0; x < unfilteredSamples.length; x++)
{
filteredSamples[x] = unfilteredSamples[x] - offset;
}
unfilteredBuffer.decrementUserCount();
return filteredBuffer;
}
}

View File

@@ -1,41 +1,96 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014,2015 Dennis Sheirer
*
* This 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/>
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
/**
* DC removal filter for removing any DC offset that may be present when a signal is not aligned within the
* demodulated channel.
*/
public class DCRemovalFilter
{
protected float mAverage;
protected float mRatio;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("DC Removal Filter");
protected float mAverage;
protected float mRatio;
public DCRemovalFilter( float ratio )
{
mRatio = ratio;
}
/**
* Creates a DC removal filter.
*
* @param ratio
*/
public DCRemovalFilter(float ratio)
{
mRatio = ratio;
}
public void reset()
{
mAverage = 0.0f;
}
/**
* Resets the running average to zero.
*/
public void reset()
{
mAverage = 0.0f;
}
public float filter( float sample )
{
float filtered = sample - mAverage;
mAverage += mRatio * filtered;
return filtered;
}
/**
* Filters the sample and returns a filtered (ie DC-removed) sample.
* @param sample to filter
* @return filtered sample
*/
public float filter(float sample)
{
float filtered = sample - mAverage;
mAverage += mRatio * filtered;
return filtered;
}
public float[] filter( float[] samples )
{
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return samples;
}
/**
* Filters the buffer samples and returns a new reusable buffer with the filtered samples.
*
* Note: this method automatically manages the user accounting between the buffer argument and the new returned
* filtered sample buffer.
*
* @param unfilteredBuffer containing samples to filter
* @return a new reusable buffer containing the filtered samples with the user count set to 1.
*/
public ReusableFloatBuffer filter(ReusableFloatBuffer unfilteredBuffer)
{
ReusableFloatBuffer filteredBuffer = mReusableBufferQueue.getBuffer(unfilteredBuffer.getSampleCount());
float[] unfilteredSamples = unfilteredBuffer.getSamples();
float[] filteredSamples = filteredBuffer.getSamples();
for(int x = 0; x < unfilteredSamples.length; x++)
{
filteredSamples[x] = filter(unfilteredSamples[x]);
}
unfilteredBuffer.decrementUserCount();
return filteredBuffer;
}
}

View File

@@ -1,71 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class DCRemovalFilter_CB implements Listener<ComplexBuffer>
{
private Listener<ComplexBuffer> mListener;
private DCRemovalFilter mIFilter;
private DCRemovalFilter mQFilter;
public DCRemovalFilter_CB( float ratio )
{
mIFilter = new DCRemovalFilter( ratio );
mQFilter = new DCRemovalFilter( ratio );
}
public void dispose()
{
mListener = null;
}
public float[] filter( float[] samples )
{
for( int x = 0; x < samples.length; x += 2 )
{
samples[ x ] = mIFilter.filter( samples[ x ] );
samples[ x + 1 ] = mQFilter.filter( samples[ x + 1 ] );
}
return samples;
}
public ComplexBuffer filter( ComplexBuffer buffer )
{
filter( buffer.getSamples() );
return buffer;
}
@Override
public void receive( ComplexBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public void setListener( Listener<ComplexBuffer> listener )
{
mListener = listener;
}
}

View File

@@ -1,61 +0,0 @@
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealBuffer;
public class DCRemovalFilter_RB extends DCRemovalFilter
implements Listener<RealBuffer>
{
protected Listener<RealBuffer> mListener;
public DCRemovalFilter_RB( float ratio )
{
super( ratio );
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
public void dispose()
{
mListener = null;
}
public float[] filter( float[] samples )
{
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return samples;
}
public RealBuffer filter( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return buffer;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
}

View File

@@ -1,54 +0,0 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.real.RealSampleListener;
public class DCRemovalFilter_R_R extends DCRemovalFilter implements RealSampleListener
{
RealSampleListener mListener;
public DCRemovalFilter_R_R( float ratio )
{
super( ratio );
}
public void dispose()
{
mListener = null;
}
@Override
public void receive( float sample )
{
if( mListener != null )
{
mListener.receive( filter( sample ) );
}
}
public void setListener( RealSampleListener listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

View File

@@ -1,60 +0,0 @@
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealBuffer;
public class DCRemovalFilter_RealBuffer implements Listener<RealBuffer>
{
private Listener<RealBuffer> mListener;
/**
* Removes DC bias present across the samples in the buffer by calculating
* the mean DC component and subtracting that component from each of the
* samples.
*/
public DCRemovalFilter_RealBuffer()
{
}
public void dispose()
{
mListener = null;
}
public RealBuffer filter( RealBuffer buffer )
{
double accumulator = 0.0d;
for( float sample: buffer.getSamples() )
{
accumulator += sample;
}
float mean = (float)( accumulator / (double)buffer.getSamples().length );
for( float sample: buffer.getSamples() )
{
sample -= mean;
}
return buffer;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
public void removeListener()
{
mListener = null;
}
}

View File

@@ -1,45 +1,105 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.buffer.ReusableBufferQueue;
import io.github.dsheirer.sample.buffer.ReusableFloatBuffer;
import io.github.dsheirer.sample.real.RealSampleListener;
public class IIRSinglePoleDCRemovalFilter implements RealSampleListener
{
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private ReusableBufferQueue mReusableBufferQueue = new ReusableBufferQueue("IIR DC Filter");
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private float mCurrentOutput = 0.0f;
private RealSampleListener mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
* 29Jan2007 paper at:
*
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
*
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
* the cutoff frequency is to DC.
*/
public IIRSinglePoleDCRemovalFilter( float alpha )
{
mAlpha = alpha;
}
@Override
public void receive( float currentInput )
private RealSampleListener mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
* 29Jan2007 paper at:
*
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
*
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
* the cutoff frequency is to DC.
*/
public IIRSinglePoleDCRemovalFilter(float alpha)
{
float currentOutput = ( currentInput - mPreviousInput ) +
( mAlpha * mPreviousOutput );
if( mListener != null )
{
mListener.receive( currentOutput );
}
mPreviousInput = currentInput;
mPreviousOutput = currentOutput;
mAlpha = alpha;
}
public void setListener( RealSampleListener listener )
{
mListener = listener;
}
/**
* Filters the sample
* @param sample to filter
* @return filtered output sample
*/
public float filter(float sample)
{
mCurrentOutput = (sample - mPreviousInput) + (mAlpha * mPreviousOutput);
mPreviousInput = sample;
mPreviousOutput = mCurrentOutput;
return mCurrentOutput;
}
/**
* Filters the input buffer and returns a new filtered output buffer.
*
* Buffer user accounting is handled by this method where the input buffer user count is decremented and the
* returned output buffer has the user count incremented to one.
*
* @param inputBuffer to filter
* @return filtered samples in a new (ie reused) output buffer
*/
public ReusableFloatBuffer filter(ReusableFloatBuffer inputBuffer)
{
ReusableFloatBuffer outputBuffer = mReusableBufferQueue.getBuffer(inputBuffer.getSampleCount());
float[] inputSamples = inputBuffer.getSamples();
float[] outputSamples = outputBuffer.getSamples();
for(int x = 0; x < inputSamples.length; x++)
{
outputSamples[x] = filter(inputSamples[x]);
}
inputBuffer.decrementUserCount();
return outputBuffer;
}
/**
* Implements the RealSampleListener interface for single sample streams.
*/
@Override
public void receive(float sample)
{
if(mListener != null)
{
mListener.receive(filter(sample));
}
}
/**
* Assigns the listener to receive filtered samples processed by the RealSampleListener interface
*/
public void setListener(RealSampleListener listener)
{
mListener = listener;
}
}

View File

@@ -1,69 +0,0 @@
package io.github.dsheirer.dsp.filter.dc;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.real.RealBuffer;
public class IIRSinglePoleDCRemovalFilter_RB implements Listener<RealBuffer>
{
private float mAlpha;
private float mPreviousInput = 0.0f;
private float mPreviousOutput = 0.0f;
private Listener<RealBuffer> mListener;
/**
* IIR single-pole DC removal filter, as described by J M de Freitas in
* 29Jan2007 paper at:
*
* http://www.mathworks.com/matlabcentral/fileexchange/downloads/72134/download
*
* @param alpha 0.0 - 1.0 float - the closer alpha is to unity, the closer
* the cutoff frequency is to DC.
*/
public IIRSinglePoleDCRemovalFilter_RB( float alpha )
{
mAlpha = alpha;
}
@Override
public void receive( RealBuffer buffer )
{
if( mListener != null )
{
mListener.receive( filter( buffer ) );
}
}
public RealBuffer filter( RealBuffer buffer )
{
float[] samples = buffer.getSamples();
for( int x = 0; x < samples.length; x++ )
{
samples[ x ] = filter( samples[ x ] );
}
return buffer;
}
public float filter( float sample )
{
float currentOutput = ( sample - mPreviousInput ) +
( mAlpha * mPreviousOutput );
mPreviousInput = sample;
mPreviousOutput = currentOutput;
return currentOutput;
}
public void reset()
{
mPreviousInput = 0.0f;
mPreviousOutput = 0.0f;
}
public void setListener( Listener<RealBuffer> listener )
{
mListener = listener;
}
}

View File

@@ -1,3 +1,18 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.design;
import io.github.dsheirer.gui.control.CurveFittedAreaChart;
@@ -58,16 +73,6 @@ public class FilterView extends BorderPane
mImpulseChart.setAlternativeColumnFillVisible(false);
mImpulseChart.setAlternativeRowFillVisible(false);
URL cssURL = FilterView.class.getResource("FilterView.css");
String css = null;
if(cssURL != null)
{
css = cssURL.toExternalForm();
}
hbox.getChildren().add(mImpulseChart);
HBox.setHgrow(mImpulseChart, Priority.ALWAYS);
@@ -81,8 +86,12 @@ public class FilterView extends BorderPane
mDFTChart.setAlternativeColumnFillVisible(false);
mDFTChart.setAlternativeRowFillVisible(false);
if(css != null)
URL url = getClass().getResource("/../resources/FilterView.css");
if(url != null)
{
String css = url.toExternalForm();
mImpulseChart.getStylesheets().add(css);
mDFTChart.getStylesheets().add(css);
}

View File

@@ -40,52 +40,49 @@ public class FilterViewer extends Application
private float[] getFilter()
{
// FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
// .sampleRate(48000)
// .sampleRate(8000)
// .gridDensity(16)
// .passBandCutoff(2500)
// .oddLength(true)
// .passBandCutoff(300)
// .passBandAmplitude(1.0)
// .passBandRipple(0.01)
// .stopBandStart(4000)
// .stopBandStart(500)
// .stopBandAmplitude(0.0)
// .stopBandRipple(0.008)
// .stopBandRipple(0.03) //Approximately 60 dB attenuation
// .build();
//
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
// .sampleRate(8000)
// .stopBandCutoff(200)
// .stopBandAmplitude(0.0)
// .stopBandRipple(0.025)
// .passBandStart(300)
// .passBandAmplitude(1.0)
// .passBandRipple(0.01)
// .build();
FIRFilterSpecification specification = FIRFilterSpecification.lowPassBuilder()
.sampleRate(48000)
.passBandCutoff(5100)
.sampleRate(50000.0)
.gridDensity(16)
.passBandCutoff(6000)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.stopBandStart(6700)
.stopBandAmplitude(0.0)
.stopBandStart(6500)
.stopBandRipple(0.01)
.build();
// FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
// .sampleRate(24000)
// .stopBandCutoff(800)
// .stopBandAmplitude(0.0)
// .stopBandRipple(0.03)
// .passBandStart(1000)
// .passBandAmplitude(1.0)
// .passBandRipple(0.08)
// .build();
// FIRFilterSpecification specification = FIRFilterSpecification.bandPassBuilder()
// .sampleRate( 48000 )
// .stopFrequency1( 200 )
// .passFrequencyBegin( 400 )
// .passFrequencyEnd( 3200 )
// .stopFrequency2( 3400 )
// .stopRipple( 0.0003 )
// .passRipple( 0.008)
// .build();
float[] taps = null;
// taps = FilterFactory.getSinc(6000, 1920, 47, Window.WindowType.BLACKMAN);
// taps = FilterFactory.getRootRaisedCosine(10, 10, 0.2f);
// taps = FilterFactory.getLowPass(48000, 6750, 7500, 60,
// Window.WindowType.HAMMING, true);
// try
// {
// taps = FilterFactory.getSincM2Synthesizer(12500.0, 2, 19);
// taps = FilterFactory.getSincFilter(25000.0, 12500.0, 2, 19, Window.WindowType.BLACKMAN_HARRIS_7, true);
// }
// catch(FilterDesignException fde)
// {
// mLog.error("Error");
// }
try
{
@@ -101,6 +98,11 @@ public class FilterViewer extends Application
mLog.error("Filter design error", fde);
}
if(taps == null)
{
throw new IllegalStateException("Couldn't design filter");
}
return taps;
}
}

View File

@@ -1,90 +0,0 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program.
* If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
package io.github.dsheirer.dsp.filter.fir.complex;
import io.github.dsheirer.dsp.filter.fir.FIRFilter;
import io.github.dsheirer.dsp.filter.fir.real.RealFIRFilter;
import io.github.dsheirer.sample.complex.Complex;
import io.github.dsheirer.sample.complex.ComplexBuffer;
public class ComplexFIRFilter extends FIRFilter
{
private RealFIRFilter mIFilter;
private RealFIRFilter mQFilter;
/**
* Complex FIR Filter for processing complex sample pairs. Wraps two real
* FIR filters for processing each of the inphase and quadrature samples.
*
* @param coefficients - filter taps
* @param gain - gain to apply to filtered outputs - use 1.0f for no gain
*/
public ComplexFIRFilter(float[] coefficients, float gain)
{
mIFilter = new RealFIRFilter(coefficients, gain);
mQFilter = new RealFIRFilter(coefficients, gain);
}
public float[] getCoefficients()
{
return mIFilter.getCoefficients();
}
public float filterInphase(float sample)
{
return mIFilter.filter(sample);
}
public float filterQuadrature(float sample)
{
return mQFilter.filter(sample);
}
public Complex filter(Complex sample)
{
float i = filterInphase(sample.inphase());
float q = filterQuadrature(sample.quadrature());
return new Complex(i, q);
}
/**
* Filters the complex samples from the buffer and returns a new complex buffer with the filtered output
*
* @param buffer with complex samples to filter
* @return new buffer containing filtered complex samples
*/
public ComplexBuffer filter(ComplexBuffer buffer)
{
float[] samples = buffer.getSamples();
float[] filtered = new float[samples.length];
for(int x = 0; x < samples.length; x += 2)
{
filtered[x] = filterInphase(samples[x]);
filtered[x + 1] = filterQuadrature(samples[x + 1]);
}
return new ComplexBuffer(filtered);
}
@Override
public void dispose()
{
mIFilter.dispose();
mQFilter.dispose();
}
}

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