Compare commits

...

138 Commits

Author SHA1 Message Date
sheirerd
5dd612edd2 #2567 Reduces memory overhead by extending the JTransforms FFT package to reuse its float array when calculaing the inverse FFT in the channelizer for a subset of the FFT size operations. 2026-11-03 05:46:12 -05:00
Denny Sheirer
0b3ce66cad #2559 User Preference setting to use complementary alias colors when dark mode causes the default alias color (black) to have no contrast on background (#2565)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-02 03:41:35 -05:00
Denny Sheirer
14c77c4926 TornadoVM experiment (#2564)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-02 03:28:44 -05:00
Denny Sheirer
8aadbe4706 #2301 am decoder enhancements. New squelch control and filter enhancements. (#2556)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 08:55:34 -05:00
Denny Sheirer
19d18b96b3 #2333 Fixes call audio prioritization (#2555)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 07:22:39 -05:00
Denny Sheirer
59e64a73ff #2338 Resolves P25P1MessageAssembler full - can't add additional dibit (#2554)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 07:09:43 -05:00
Denny Sheirer
8b338dec2d #2373 DMR stale talker alias (#2553)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 06:47:22 -05:00
Denny Sheirer
bab02222a4 #2394 ControllerPanel clipping on OSX (#2552)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 06:34:51 -05:00
Denny Sheirer
9359f523c1 #2421 Motorola talker alias with Cyrillic now decodes correctly (#2551)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 06:20:47 -05:00
Denny Sheirer
e385d2fdec #2438 resolves broadcaster error that happens during tuner source swapout (#2550)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 06:04:57 -05:00
Denny Sheirer
49c3d3ff55 #2466 Gradle build now checks that user has JDK with bundled JavaFX libraries installed (#2549)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 05:49:58 -05:00
Denny Sheirer
7b249af944 #2496 Displays use complementary color for aliases in dark mode (#2548)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 05:38:48 -05:00
Denny Sheirer
197cec04f3 #2529 NXDN adds aliasing support for Type-D identifiers and fixes issue in certain NXDN messaing where the TO identifiers are incorrectly tagged. (#2547)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 05:21:28 -05:00
Denny Sheirer
c75baa48f4 #68 waterfall now updates at all zoom levels (#2546)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 04:46:51 -05:00
Denny Sheirer
866aadc8c3 ##2544 bladeRF supports up to 122 MHz sample rate. Adds new 2048 and 4096 half band decimation filters. (#2545)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-11-01 04:24:22 -05:00
Denny Sheirer
276f4a316a #2542 bladeRF SDR tuner support (#2543)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-31 08:24:45 -04:00
Denny Sheirer
e65ff5a8dc #2540 HydraSDR tuner uses jHydraSDR library and removes legacy java driver code. (#2541)
- Uses libhydrasdr HIGH_DEFINITION decimation mode by default.
- Tuner controller extends TunerController since libhydrasdr owns the USB device, transfers and real to complex sample conversion.
- Tuners are still discovered by USB bus and port. The controller reads the USB serial string and opens the device by serial number.
- Samples are scaled by 2 to preserve existing levels (-6 dB otherwise).
- Streaming is stopped for sample rate changes because libhydrasdr replaces the sample converter in use by its streaming thread.
- Buffer sample count follows the delivered power-of-2 buffer size and adds the 1.25 MHz sample rate.

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-31 06:15:02 -04:00
Denny Sheirer
85bfd21257 2537 Complex Vector Oscillator (256/512) Lanes Can Drift Apart Over Time (#2538)
* #2535 Heterodyne Channelizer enhancements and corrections.

* #2537 Complex vector oscillator SIMD lane drift.

Realigns the SIMD lanes every 262,144 samples. At frequencies where the
rotation for a set of lanes is nearly a whole or half turn, rounding affects
each lane differently and the lanes drift apart until the next frequency
change, producing spurs at multiples of the sample rate divided by the lane
count.  Lane phase error at 3125 Hz with a 50 kHz sample rate was about -38 dBc
after one hour and is now about -83 dBc with 16 lanes and -73 dBc with 8 lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* #2537 test updates

* #2537 test updates

---------

Co-authored-by: denny <dennis.sheirer@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 08:42:49 -04:00
Denny Sheirer
9ebbf957a2 #2533 Polyphase channelizer enhancements for correctness and efficiency. (#2534)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-30 06:47:50 -04:00
Denny Sheirer
00c0418017 #2531 NXDN Talkgroup Zero Is Valid (#2532)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-30 04:52:31 -04:00
Denny Sheirer
33a2471338 #2521 JMBE comfort noise insertion default gain (#2530)
Updates to JMBE library 2.1.0
Cleanup AMBE/IMBE sections in each editor P25, NXDN and DMR

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-30 04:26:24 -04:00
GTR8000
d86fba6945 Add optional legacy JMBE tone levels to digital channel editors (#2511)
Persist the per-channel selection, preserve it when copying configurations, and transfer it to mixed-phase P25 traffic channels. Apply only when the loaded codec advertises support. Calibrated levels remain the default.
2026-10-30 03:43:36 -04:00
Denny Sheirer
248fe3e4f7 #2526 Polyphase Channelizer gain imbalance across synthesis of two adjacent channels. (#2527)
- Fix two-channel synthesis asymmetry across polyphase channel boundaries.
- TwoChannelSynthesizerM2: replace incorrect 2-path polyphase split (which applied
  different responses to each channel) with channel sum + prototype filter
  modulated to the channel boundary (+FS/4).
- ComplexPolyphaseChannelizerM2: start on the middle block map when taps per
  channel is odd so adjacent channel outputs are phase coherent.
- TwoChannelOutputProcessor: translate channel 2 by FS/2 using the absolute
  block index parity derived from the even-sized dispatch batches.
- SynthesizerViewer: translate channel 2 before synthesis.
- Add TwoChannelSynthesizerM2Test for flat, symmetric boundary response.

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-29 08:51:37 -04:00
Denny Sheirer
9a6dc2b0a0 #2524 P25P2 Decoder enhancements (#2525)
* Adopts same architecture and symbol processor as P1 C4FM/LSM, DMR and NXDN.
* Soft symbol sync detection, processing and erro correction.
* Scalar and vector soft sync detection and calibration
* Improved time slot detection/prediction and ISCH processing.
* Fixes bug in parsing sequential mac structures in SACCH
* Vector calibration(s) cleanup across multiple calibrations
* Error correction bug in ISCH
* Renames differential demod shared with P1 LSM decoder.

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-29 07:32:17 -04:00
Denny Sheirer
6b9cd2d317 #2518 P25P1 LSM Decoder enhancements. (#2523)
* Uses same shared architecture and symbol processor pattern as other C4FM decoders.
* Adds soft symbol processing and sync detection.
* Removes RRC filter and replaces with improved channel filter.
* Viterbi soft symbol decoding with demodulator reliability weighting
* Improved sync reacquisition in low SNR
* Improved handling for phase wrapping and interpolation
* NAC Tracker updated to track by NAC count
* Native buffers returning partial buffers
* Message processor uses only valid LDU1 words
* Reduced phantom TDU messages
* Tuner/Channe frequency error corrections don't reset the symbol trackers and fix possible divide by zero error
* LSM timing error detector now updates per symbol block size instead of sample buffer size - faster convergence
* New unit tests
* Resolves several issues, including a lagging sync detect offset issue in the recently updated P25P1 C4FM decoder.
* Consolidate filter designers for each protocol back into the FilterFactory class.
* DMR, P25 Phase 1 C4FM and LSM, and P25 Phase 2 decoders design the baseband filter once and share the taps across the I and Q filters.

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-29 04:17:47 -04:00
Denny Sheirer
f2c98cdb40 #2516 DMR Decoder enhancements. Architecture now shares common C4FM framework like P25P1 and NXDN. Adds soft decoding and enhanced/corrected error detection and correction. (#2517)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-26 06:34:57 -04:00
Denny Sheirer
5635341d12 #2494 P25P1 and NXDN Decoder Enhancements. (#2514)
- Updates P25P1 C4FM decoder to use same pattern developed for NXDN.
- Both share the C4FM symbol processor and equalizer.
- P25P1 updated with Garner style timing error detector
- NXDN filters updated to use a combined RRC and inverse sync filter all in one.
- P25P1 replaces RRC with band limiting filter
- P25P1 enhanced with dynamic PLL balance updates to combat multi-path fading, in addition to the sync detection training window.
- Decoder PPM error reports now only sent after 2x consecutive sync detections to prevent tuner PPM corrections from noisy/isolated false sync detects.
- Soft error correction added to both P25P1 and IDEN.

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-25 09:44:50 -04:00
Denny Sheirer
df739f525f Pin nightly release host JDK to 27.0.0 (#2515)
The nightly release build fails in the :jre task because setup-java now
resolves java-version '27' to Liberica 27.0.1, while runtimeZipOthers and
runtimeZipWindows download 27+36 (27.0.0) target JDKs. jlink requires the
target java.base version to exactly match its own version, so it exits 1.

Pin the host JDK to 27.0.0 so it matches the downloaded JDKs. Bump both
together when updating jdk_download_base in build.gradle.


Claude-Session: https://claude.ai/code/session_01TNQURTPv1or2s6qjnk4NMB

Co-authored-by: Dennis Sheirer <dennis.sheirer@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 04:36:54 -04:00
Denny Sheirer
c8b1c8ed3a #2512 NXDN Call and GPS events not aliasing correctly (#2513)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-24 05:04:12 -04:00
Denny Sheirer
b0428a1d8b #2509 Tuner frequency control highlight color adapts to color theme (#2510)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-20 03:46:53 -04:00
GTR8000
c0a96f94fb Load JMBE V2 codecs so audio controls take effect (#2505)
* Load JMBE V2 codecs so audio controls take effect

Prefer the V2 library and codec factory, retaining fallback support for JMBE 1.x. Log codec class and supported features. Verified tone gain at zero and unity with JMBE 2.0.2, and digital audio with JMBE 1.x.

* Handle missing JMBE library explicitly in V1 fallback
2026-10-20 03:37:43 -04:00
Denny Sheirer
1945b83562 #2503 JFrequencyControl digits too wide in dark mode (#2504)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-18 05:37:38 -04:00
Denny Sheirer
dc550f3121 #2499 JMBE settings are now carried over when cloning channel configurations and P25 JMBE settings now carry over from the parent control channel configuration when traffic channels are a mix of phase 1 and phase 2. (#2502)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-18 04:52:14 -04:00
Denny Sheirer
0460f2b77b Theme-aware icons for the main window menu bar and Playlist Editor tab (#2493)
The main window's font-glyph icons were rendered with IconFontSwing.buildIcon,
which bakes a fixed colour into the image (black by default, DARK_GRAY for the
Playlist Editor tab), so the Screen Capture camera, the Playlist Editor tab icon
and the View menu icons ignored the selected theme and were nearly invisible on
dark themes.

Adds ThemedFontIcon, which resolves its colour from UIManager at paint time
(Label.foreground, MenuItem.selectionForeground while a menu item is armed,
Label.disabledForeground when disabled) and re-renders only when the colour
changes, so icons follow theme switches without a restart.


Claude-Session: https://claude.ai/code/session_01TDsxnQntujmycg3aJyRsuN

Co-authored-by: Dennis Sheirer <dennis.sheirer@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 06:05:18 -04:00
farewell-ladmin
b37ed51d07 Fix Material Deep Ocean theme class name (#2479)
FlatLaf 3.x renamed the Material Theme UI Lite classes with an MT prefix,
so selecting "Material Deep Ocean" threw ClassNotFoundException and fell
back to FlatLightLaf:

  ThemeManager - Unable to instantiate LAF [...materialthemeuilite.
  FlatMaterialDeepOceanIJTheme] for theme MATERIAL_DEEP_OCEAN

The class in flatlaf-intellij-themes 3.7.2 is FlatMTMaterialDeepOceanIJTheme.
All other theme classes in Theme verified present in the 3.7.2 jars.


Claude-Session: https://claude.ai/code/session_01FRdfTvgcdBvwkipdfP8qxy

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 05:52:11 -04:00
Denny Sheirer
f1183e531d #2491 JDK 27 and Gradle 9.8.1 (#2492)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-12 05:45:02 -04:00
Denny Sheirer
16a66112fc #2489 P25 Phase 1 excessive audio gain (#2490)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-11 08:05:04 -04:00
Denny Sheirer
8aa6ae089a #2487 JMBE creator adds diagnostic logging (#2488)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-11 03:27:10 -04:00
Denny Sheirer
728a83a82d #2484 JMBE version 2 control in channel protocol editors. (#2485)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-10 06:22:10 -04:00
Denny Sheirer
c05328cd1e #2482 JMBE Library version 2.0 (#2483)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-10-09 10:51:29 -04:00
AJ Collins
a9120ed65e Icecast 2.4+: Fix Icecast 2.5 HTTP PUT connect (#2470)
Icecast 2.5 sends the final PUT response only after audio is received, so it never reached connected status. The request now sends Expect: 100-continue and starts streaming on a continue response.

Also updated the regex pattern in the response parser so it matches both HTTP/1.0 and 1.1.
2026-09-26 04:42:37 -04:00
Steve Pinkham
9305abc784 P25 Phase 2: consult I-ISCH 2 when I-ISCH 1 fails to decode (#2458)
SuperFrameFragment.getTimeslotOffset() tests getIISCH1().isValid() twice,
so the second I-ISCH is never consulted and a fragment whose first I-ISCH
does not decode is descrambled at timeslot offset 0.  On a weak signal
the first I-ISCH fails on almost half of the fragments, and two of every
three of those are then descrambled with the wrong phase.  Test I-ISCH 2
as the surrounding code intends (isFinalFragment() already does).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 04:26:29 -04:00
GTR8000
0c637ed80d Track Broadcastify duplicate-rejected calls (#2467)
Co-authored-by: Codex <codex@openai.com>
2026-09-26 04:17:39 -04:00
Denny Sheirer
7ee3c5d251 #2473 P25P1 bad frequency bands messages contaminating channel frequency calculation map (#2474)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-09-26 04:05:26 -04:00
GTR8000
a33c99347b Fix MDC1200 emergency activity summary loop (#2468) 2026-09-26 03:04:40 -04:00
Randy
97cae50bdd NBFM Channel level CTCSS/DCS and audio features (#2425)
NBFM decoder enhancements.  CTCSS and DCS squelch control.  Adds deemphasis, high pass filter control, and auto-level control
2026-09-26 02:54:31 -04:00
Denny Sheirer
80360029ef #2449 JDK 26 Gradle 9.6.1 and Library Updates (#2450)
* #2449 JDK 26 Gradle 9.6.1 and Library Updates
2026-08-01 08:25:57 -04:00
Akumas_Coffin_
af5dbcfcdc Dark mode (#2411)
* Add dark mode toggle under User Preferences > Application

- Add darkMode boolean preference to ApplicationPreference (persisted via java.util.prefs)
- Add ToggleSwitch labeled "Dark Mode" to ApplicationPreferenceEditor
- Add ThemeManager singleton that listens for preference updates and applies/removes
  the dark stylesheet to all registered JavaFX Scenes and swaps the Swing LAF
- Add sdrtrunk_dark.css with dark styling for controls, tables, menus, dialogs, charts
- Add FlatLaf dependency for full Swing dark theming (replaces previous OS-specific
  MetalLookAndFeel setup; FlatLaf works cross-platform)
- Register all Stages created in JavaFxWindowManager with the ThemeManager and walk
  Window.getWindows() at apply-time so unregistered dialogs/alerts also pick up the theme
- Add Foojay toolchain resolver to settings.gradle so Gradle can auto-provision JDK 25

* Fix dark-mode startup failures on Linux

- Apply Swing LAF synchronously on the calling thread so the main JFrame's RootPane
  has a populated UIDefaults set when constructed.  Previously the LAF install was
  scheduled via EventQueue.invokeLater, so JFrame construction raced ahead and threw
  "no ComponentUI class for: javax.swing.JPanel".
- Call LookAndFeelFactory.installJideExtension(VSNET_STYLE) with an explicit style
  instead of the no-arg auto-detection variant.  The auto-detection path probes
  com.sun.java.swing.plaf.windows.WindowsLookAndFeel via Class.forName, which is
  absent on non-Windows JDKs and threw ClassNotFoundException at startup.
- Bounce only the SwingUtilities.updateComponentTreeUI walk through the EDT so it
  redraws already-realized windows on theme toggle.

* Fix JIDE/Windows-LookAndFeel crash on non-Windows JDKs

JIDE 3.6.18's LookAndFeelFactory contains compile-time
\`instanceof com.sun.java.swing.plaf.windows.WindowsLookAndFeel\` checks in
multiple methods, including a JIDE PropertyChangeListener that fires on every
UIManager.setLookAndFeel call.  On Linux and macOS JDKs the class is genuinely
absent from java.desktop, so the JVM throws NoClassDefFoundError the first time
JIDE's dispatch code is executed (during JFrame init and again from
JideSplitPane.updateUI on each LAF change).

Two complementary fixes:

1. Pass an explicit MetalLookAndFeel instance to the 3-arg
   installJideExtension(UIDefaults, LookAndFeel, int) overload so JIDE's Metal
   branch matches first and never falls through to the Windows branch in the
   normal install path.

2. Add a no-op stub class at com.sun.java.swing.plaf.windows.WindowsLookAndFeel
   so that the compile-time class reference resolves on non-Windows JDKs.  The
   stub reports isSupportedLookAndFeel = false and is never instantiated -
   JIDE only uses it for instanceof comparisons against the active LAF, which
   always return false in our setup.  On Windows JDKs the real class in
   java.desktop is loaded by the platform classloader and shadows this stub.

* Re-apply FlatLaf defaults after JIDE install + strengthen FX borders

Symptom: With dark mode enabled, the Swing main window's spectrum chrome and
menu bar were dark (FlatLaf) but tables, tabbed panes and the inner content
area were light/Metal coloured.

Root cause: LookAndFeelFactory.installJideExtension installs JIDE's
Metal-flavored UI defaults (VsnetMetalUtils etc.) into the live UIDefaults
table, overwriting FlatLaf's standard component colors and fonts for keys
like Table.background, TabbedPane.foreground, Panel.background.

Fix: After the JIDE install, walk a fresh FlatLaf UIDefaults table and put
every key back into the active defaults, skipping anything Jide-prefixed.
JIDE's UI delegate registrations live under Jide* keys that FlatLaf does
not define, so they survive untouched while the standard Swing color and
font keys are restored to their dark equivalents.

Also strengthen the JavaFX dark stylesheet: explicit borders on text fields,
tables, titled panes, split-pane dividers and separators so components have
more defining outlines in dark mode.

* Force-override Swing foreground colors via UIManager user-defaults layer

Symptom: With dark mode enabled the inner Swing content area now had dark
panels (good) but tab labels, table cells, status-bar labels and button
text were rendering nearly black-on-dark (bad).

Root cause: UIManager.getLookAndFeelDefaults() lookups for FlatLaf's
component foreground keys still resolve to Metal-era dark values after the
JIDE install + FlatLaf re-apply, because some FlatLaf keys are derived /
LazyValue and recompute against state that JIDE seeded.

Fix: After the bulk FlatLaf-defaults copy, explicitly UIManager.put the
foreground / background / selection / disabled / border colors for every
standard Swing component family.  UIManager.put writes to the user-defaults
layer that takes precedence over the LAF defaults, so these overrides win
regardless of what JIDE or derived-value resolution produces.

Also bump the JavaFX dark stylesheet text colour from #d6d6d6 to #ececec so
labels read closer to white on the dark panels.

* Theme JIDE-specific UIManager keys so JideTabbedPane reads dark colors

The Now Playing / Map / Tuners / Playlist Editor / Details / Events /
Messages / Channel tabs are JIDE JideTabbedPane components and their UI
delegate reads JideTabbedPane.* UIManager keys, not the standard
TabbedPane.* keys.  The previous overrides only set the standard keys so
the tabs were still rendering with JIDE's Metal-era dark foreground on
the now-dark background.

Override the JIDE-specific text foreground keys
(JideTabbedPane.foreground, .selectedTabTextForeground,
.unselectedTabTextForeground, .activeTabTextForeground) along with the
matching background and shadow keys.  Also theme JideSplitPane,
JideButton and JideLabel keys for consistency.

* Remove hardcoded Color.BLACK foreground on JideTabbedPanes

ControllerPanel.getTabbedPane() and NowPlayingPanel.getTabbedPane() each
called setForeground(Color.BLACK) on their JideTabbedPane instances.

Color.BLACK is a plain java.awt.Color, not a ColorUIResource, so it is
never replaced by LookAndFeel.installColorsAndFont() on subsequent UI
installs - it permanently pins the tab labels to black regardless of any
JIDE or FlatLaf defaults the theme manager sets.  In dark mode this leaves
the tab labels black-on-dark and unreadable.

Drop the explicit foreground calls so the LAF (JideTabbedPaneUI reading
JideTabbedPane.foreground / .unselectedTabTextForeground /
.selectedTabTextForeground / .activeTabTextForeground) drives the colour
in both light and dark modes.

* Register Channel tab's JFXPanel scenes with the ThemeManager

The Channel tab embeds two JavaFX scenes inside Swing via JFXPanel: the
noise squelch chart (NoiseSquelchView) and the symbol view.  Neither was
registered with the ThemeManager, so the dark stylesheet was never applied
to them.  In dark mode the chart legend, axis area and surrounding panel
rendered with JavaFX's default light Modena styling against the otherwise
dark UI.

JFXPanel-hosted scenes are invisible to javafx.stage.Window.getWindows(),
so the theme manager cannot find them automatically on toggle - they have
to be explicitly registered at creation time, the same as the JavaFX
Stages JavaFxWindowManager already registers.

* Use ColorUIResource for theme overrides + brighten chart series colors

Toggle-off problem: applyExplicitOverrides was storing plain java.awt.Color
values via UIManager.put.  LookAndFeel.installColorsAndFont only replaces a
component's existing colour when it is null or an instance of UIResource -
plain Colors are not, so once a component picked up its dark colour it kept
that colour for the rest of the JVM lifetime even after toggling back to
light mode.  Switching every override to ColorUIResource lets subsequent
updateComponentTreeUI calls replace the cached colour cleanly.

Chart visibility: the JavaFX charts on the Channel tab (NoiseSquelchView)
were inheriting Modena's default 8-colour series palette, which has several
hues that read poorly against a dark plot background.  The existing
sdrtrunk_style.css also hardcodes .chart-symbol to solid black.  Override
.default-color0..7.chart-series-line and matching .chart-line-symbol selectors
with a brighter high-contrast palette, darken the plot background to #1c1c1c
for stronger contrast against the grid lines, and add explicit colours for
axis tick marks, alternate row fills and legend border.

* Use primary foreground for selected tab text instead of selection foreground

The previous overrides set JideTabbedPane.selectedTabTextForeground and
TabbedPane.selectedForeground to fgSelection (always white).  Selected tabs
render on the raised surface (bgRaised) which is white in light mode, so
white-on-white made selected tab labels invisible.

Selected tabs are not list/table row selections that use the blue
bgSelection background - they sit on the same raised tone as other surfaces.
Use fgPrimary so the text is dark on the light raised surface in light mode
and light on the dark raised surface in dark mode.

* Brighten chart data points to solid white in dark mode

* Restore default chart symbol size, only override colour

* Refresh detached Channel-tab swing components on theme toggle

The Channel tab swaps one of three right-pane components into its
JSplitPane depending on the active decoder type: SignalPowerView for AM,
the NoiseSquelchPanel JFXPanel for NBFM, the SymbolPanel JFXPanel for
feedback decoders.  Only one is attached at any moment - the other two
are detached references on the ChannelSpectrumPanel.

SwingUtilities.updateComponentTreeUI(Window) during a theme toggle walks
only the currently-realized component trees, so the detached swappables
miss the refresh and carry stale colours from the LAF that was active
when they were last attached.  Swapping back in then shows e.g. an AM
SignalPowerView in dark colours after a light-mode toggle.

Add ThemeManager.registerSwing(Component) tracking, walk those refs on
every toggle, and register the three swappables when constructed.

* Add preset themes selector under User Preferences > Application

Replace the single Dark Mode toggle with a curated selection of 9 preset
themes, exposed as a vertical radio-button group:

  Light                IntelliJ              Nord
  Dark                 Darcula               Dracula
  Solarized Light      Solarized Dark        Monokai Pro

Each Theme enum value pairs a human-readable display name with a FlatLaf
class name and a dark flag.  The first four come from the core flatlaf
dependency; the others come from flatlaf-intellij-themes which is added
as a new implementation dependency (~1MB).

ApplicationPreference now stores the selected theme by enum name under
the new ui.theme key.  isDarkMode() is retained as a compatibility shim
that delegates to the current theme.  Existing users with the legacy
dark.mode boolean are auto-migrated on first read.

ThemeManager is refactored to drive the LAF from the Theme:
- Reflectively instantiates the FlatLaf class.
- Reads Panel.background, Button.background, TextField.background,
  List.selectionBackground/Foreground, Label.foreground/disabledForeground,
  Component.borderColor and Table.alternateRowColor from UIManager *after*
  FlatLaf installs, then seeds those values into the JIDE-specific keys.
  Each theme automatically contributes its own palette to JIDE widgets
  without per-theme hardcoded colours.

ApplicationPreferenceEditor swaps the ToggleSwitch for a ToggleGroup of
RadioButtons that persist the selection through ApplicationPreference.setTheme.

* Fix theme switching past first toggle + move Themes to Appearance sub-page

Critical regression in 56b0f6c9: applyExplicitOverrides reads colours via
UIManager.getColor(...) which consults the merged defaults (user defaults
overlay LAF defaults).  After the first apply our own UIManager.put calls
populate the user-defaults layer, so subsequent reads return our previous
theme's colours instead of the newly-installed LAF's colours.  The result:
the first theme switch works, every switch after that silently re-applies
the previous palette and the UI appears frozen on the first picked theme.

Switch every palette read to UIManager.getLookAndFeelDefaults().getColor(...)
which is the LAF-only layer and ignores the user-defaults overlay we wrote.

Also:
- Move the Themes selector to its own sub-node:
    User Preferences > Application > Appearance
  PreferenceEditorType gets a new APPEARANCE entry, the existing APPLICATION
  leaf is relabelled "General" so the tree no longer reads "Application >
  Application > Application".  New AppearancePreferenceEditor hosts the theme
  radio group; ApplicationPreferenceEditor loses it.

- Replace a few hardcoded label foregrounds that read as
  invisible-on-dark-bg with LAF-driven equivalents:
    TunerViewPanel TunerStatus.DISABLED: DARK_GRAY -> Label.disabledForeground
    BroadcastStatusPanel DISABLED state: LIGHT_GRAY -> Label.disabledForeground
    JFrequencyControl Digit mouseExited:   Color.WHITE -> TextField.background

  Semantic colour usage (RED/YELLOW for error and warning states, the
  audio-panel hardware-style black backgrounds) is intentionally kept.

* Auto-theme every JavaFX window so transient dialogs follow the theme

The Playlist Editor's main Scene is already registered with the ThemeManager,
but the many Alert / TextInputDialog / RadioReferenceUnavailableAlert popups
it spawns each create a fresh Stage with a fresh Scene that nothing registers.
Once one is shown after a theme switch it renders with default Modena styling
on top of an otherwise themed app.

Install a ListChangeListener on javafx.stage.Window.getWindows() at startup.
Every newly-added Window is themed immediately if its Scene is set, otherwise
a one-shot sceneProperty listener applies the theme the moment the Scene is
attached.  Already-open windows at install time are also walked once.

This removes the need to manually register every transient dialog and covers
the playlist editor, alias editor confirmations, broadcastify error alerts,
channel-map editor warnings, and any future ad-hoc Alert / Dialog the
codebase grows.

* Curate themes to 8 modern variants + per-theme JavaFX accent colours

Theme set is trimmed and refocused:
  Drop:  IntelliJ (too similar to Light), FlatDarculaLaf (duplicates Dracula),
         Monokai Pro (low identity in this app).
  Add:   Material Deep Ocean (vibrant teal dark), Arc Dark (flat modern dark).
  Keep:  Light, Dark, Nord, Dracula (FlatDraculaIJTheme - the one user liked),
         Solarized Light, Solarized Dark.

Each theme now contributes its own accent palette to the JavaFX panels too,
not just the Swing chrome.  ThemeManager builds a small per-theme stylesheet
at apply time by reading the FlatLaf-installed colours (Panel.background,
TextField.background, Component.accentColor, List.selectionBackground/Foreground,
Label.foreground, Component.borderColor, Table.alternateRowColor) and
substituting them into Modena's core variables (-fx-base, -fx-accent,
-fx-selection-bar, -fx-control-inner-background, etc.) plus a handful of
explicit rules for table selection rows, default button, scroll bar thumb.

The generated CSS is attached to every registered Scene as a data: URL, so
Nord reads blue-tinted, Dracula purple-tinted, Solarized cream/teal,
Material Deep Ocean dark teal, Arc Dark cool gray.  The static
sdrtrunk_dark.css remains as the structural baseline for all dark themes.

* Add GUI scale slider under Appearance preferences

New slider in User Preferences > Application > Appearance lets users zoom
the whole UI between 50% and 200% in 10% increments.  The value is
persisted as a new ApplicationPreference (ui.gui.scale, default 1.0,
bounded 0.5-2.0) and applied immediately on slider release - intermediate
drag positions do not trigger a re-apply so the LAF reinstall cost is
paid once at the end of the drag.

Swing side: ThemeManager sets the flatlaf.uiScale system property before
each setLookAndFeel call, so the new FlatLaf instance picks up the user's
zoom on initialise.  All component sizes (fonts, padding, icons) scale
together because FlatLaf does the math internally.

JavaFX side: the per-theme accent stylesheet emits an
  .root { -fx-font-size: <12 * scale>px; }
rule.  Modena's component sizes are em-relative so cascading the font
size scales the JavaFX panels in sync with the Swing chrome.

The onPreferenceUpdated guard now compares both theme and scale, so
moving only the slider still triggers a re-apply.  A Reset button next
to the slider returns to 100%.
2026-07-31 04:20:40 -04:00
James Yuzawa
9dcebb4943 Fix eclipse build (#2434)
Fixes imports and other issues highlighted by Eclipse IDE
2026-07-31 03:51:54 -04:00
Denny Sheirer
e2d9c4cda3 #2398 Sample Buffer code error (#2447)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-07-30 02:53:11 -04:00
Denny Sheirer
3e21887d27 #2435 Radio reference editor now creates NXDN repeater channel configurations from non-trunked county entries (#2446)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-07-29 03:49:55 -04:00
Denny Sheirer
d60720aab7 2441 Removes logging from NXDN talker alias assembler. (#2442)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-07-13 04:21:58 -04:00
John Stockdale
2292af6c07 #2428 Polyphase Channelizer efficiency enhancements
Closes #2428 

Fuse polyphase channelizer multiply-accumulate and reuse scratch accumulator to reduce per-block allocation (#2429)
2026-07-12 07:38:20 -04:00
James Yuzawa
ff128f56d2 Add JVM autoprovisioning in Gradle (#2427)
* add bellsoft jvm autoprovisioning

* remove redundant srcsets
2026-07-12 07:21:59 -04:00
Paul Donahue
3c01c644c9 Extend DCS code list to include 104 commonly used codes for modern radios (#2424) 2026-07-12 06:37:37 -04:00
Rishi Raj
ade4dc47cd #2342 Playlist file name is now sortable
Closes #2342

Updates playlist manager playlist name column to be sortable.
2026-07-12 06:35:14 -04:00
Uğur Tafralı
cbbbe3a131 Clear frequency band map when switching P25 control channels (#2404) 2026-07-12 06:21:44 -04:00
Alex Bloom
b385a42b63 Integrate OTA alias for Broadcastify srcId_alias (#2395)
* Add srcId_alias (source radio alias) to Broadcastify calls uploads

Ports the OTA alias lookup logic from trunk-recorder commit 91597dc into
sdrtrunk's Broadcastify Calls integration. When uploading a call, the
source radio ID's alias name is resolved via the existing alias system
and sent as the srcId_alias form field. If no alias is configured, the
field is omitted.

Agent-Logs-Url: https://github.com/djshadowxm82/sdrtrunk/sessions/abc044d3-99c6-4d5c-8d44-99ac8a11bef9

Co-authored-by: djshadowxm82 <10658965+djshadowxm82@users.noreply.github.com>

* Use OTA TalkerAliasIdentifier for srcId_alias instead of manual AliasList lookup

Changes getFromAlias() to resolve the over-the-air talker alias acquired
in real-time from RF signals (P25 Motorola Talker Alias, DMR Talker
Alias) instead of looking up manually pre-configured aliases from the
AliasList system. Follows the same pattern used by OpenMHZ and
RdioScanner broadcasters.

Agent-Logs-Url: https://github.com/djshadowxm82/sdrtrunk/sessions/d8cbaf9c-e6db-419a-90f4-c9ee04846995

Co-authored-by: djshadowxm82 <10658965+djshadowxm82@users.noreply.github.com>

* Add build-test workflow for downloadable release artifacts on this branch

Agent-Logs-Url: https://github.com/djshadowxm82/sdrtrunk/sessions/224c0998-9677-4b83-8fb4-724c1e745e00

Co-authored-by: djshadowxm82 <10658965+djshadowxm82@users.noreply.github.com>

* Remove workflow file

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: djshadowxm82 <10658965+djshadowxm82@users.noreply.github.com>
2026-07-11 05:46:43 -04:00
Randy
ccae1d05e7 Bugfix-#2383-audio-trapped-in-output-buffer-of-resampler (#2384)
Closes #2383

Resolves issue with audio samples trapped in the resampler output buffer. Includes improvement provided by @bazineta
2026-07-11 05:27:15 -04:00
Uğur Tafralı
788d994ad7 Fix #2377: add null check for mAudioOutput in processAudioSegments to prevent NPE when no audio outputs present (#2378)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-11 04:06:18 -04:00
msullivan1993
7cf6974b6b Adds non standard DCS code and "Select Tone" placeholder (#2357)
* Add non-standard DCS code D212N

* Add 'Select Tone' default placeholder when adding new DCS identifier
2026-07-11 03:33:18 -04:00
TimmySheff
cf0737ed90 Add Alias List column to the playlist table (#2436)
Co-authored-by: tsinmw <tsinmw2@gmail.com>
2026-07-11 03:25:20 -04:00
Denny Sheirer
75fa495c99 #2432 NXDN cleanup. Adds jdk.charsets module to build script and updates NXDN radio ID tooltip values. (#2433)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-07-09 03:45:26 -04:00
Denny Sheirer
7945f0ce63 #7 NXDN Decoder support. Redesigns tuner and channel frequency error management. (#2431)
Closes #7 

NXDN Decoder

* Implements: NXDN4800, NXDN9600, Type-C and Type-D decoding and trunk tracking.
* Redesigns tuner and channel frequency error management.
2026-07-08 03:44:08 -04:00
Denny Sheirer
a053315675 #2364 Audio Playback supports 64-line alsa mixers as stereo output (#2374)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-02-19 03:54:10 -05:00
Denny Sheirer
de0e722f0c #2305 P25P1 Audio Squeak caused by false sync detection. (#2362)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-02-10 03:16:08 -05:00
Denny Sheirer
a42592daa9 #2356 Muted audio channel can leak audio (#2361)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-02-09 03:02:41 -05:00
denny
7f9dc86b23 Revert "#2356 Muted audio channel can leak audio"
This reverts commit 185df1735a.
2026-02-09 02:54:23 -05:00
denny
2f5c165aec Revert "#2356 Muted audio channel can leak audio"
This reverts commit 29164ac75d.
2026-02-09 02:54:16 -05:00
denny
29164ac75d #2356 Muted audio channel can leak audio 2026-02-09 02:52:09 -05:00
denny
185df1735a #2356 Muted audio channel can leak audio 2026-02-09 02:50:28 -05:00
Denny Sheirer
c81e6cccfa #2356 Audio playback redesign. Resolves audio playback issue on some (e.g. Kubuntu) systems. For stereo playback, (#2359)
consolidates left and right channels into a single playback channel.  Implements continuous audio streaming using
silence to fill the gaps to avoid mixer source data line start/stop issues.  Code structure is ready to layer on
multi-channel (ie 5.1 or 7.1) audio playback devices, but that hasn't been tested.

Co-authored-by: denny <dennis.sheirer@gmail.com>
2026-02-08 05:30:39 -05:00
Denny Sheirer
a2509554db #2340 Update map plotting to address glitchy behavior when selecting entities and histories. (#2341)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-12-26 05:40:57 -05:00
tadscottsmith
b83f77c7c6 #2092 Adjust P25 P1 Motorola Talker Alias Dispatch Sequencing (#2317)
* Add logic to ensure last Motorola talker alias block is handled before the completed alias.

* Return removed comment.
2025-12-25 04:25:51 -05:00
Denny Sheirer
d8e2e0ac57 Hydrasdr Tuner Support (#2339)
HydraSDR tuner support
* Adds Bias-T option
* Update nightly.yml
* Fix BoardID
* Consolidated calibrations and sample converters and removed logging.

---------
Co-authored-by: Benjamin Vernoux <bvernoux@gmail.com>
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-12-25 04:18:12 -05:00
Denny Sheirer
b0a0ad4b43 Update README.md 2025-12-05 16:24:36 -05:00
Denny Sheirer
17cdd0df27 Update README.md 2025-12-05 16:07:38 -05:00
Denny Sheirer
6d23aaa06f Update README.md (#2330) 2025-12-05 15:55:46 -05:00
Denny Sheirer
933629e30d #2324 JMBE creator support for MacOS x86-64 (#2325)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-29 05:56:38 -05:00
Denny Sheirer
8a6bc03ffc 2322 Tuner Manager logs MacOS Tahoe how-to fix when LibUSB doesn't get loaded. (#2323)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-29 05:37:43 -05:00
Denny Sheirer
0c616d4531 2320 Mac OS Tahoe usb4java library update for legacy aarch64 support. (#2321)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-29 04:19:29 -05:00
AJ Collins
b472c68b80 #1892 Icecast 2.4+: Fix retries after network disruption (#2279)
* #1892 Icecast 2.4+: Fix retries after network disruption

* #1892 Icecast 2.4+: Log the connection attempt/failure once and suppress subsequent logging until reconnected
2025-11-24 03:28:33 -05:00
Denny Sheirer
f6f4b07623 #2281 Updates P25 Adjacent Site Status logging format between TSBK and AMBTC formats (#2310)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-22 06:00:00 -05:00
Denny Sheirer
de100b4662 #2304 sdrplay api now handles device failure without crash (#2309)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-22 05:44:19 -05:00
Alan Crosswell
bbde3033b7 try to get resizing to be remembered and restored. (#2269)
Per tsinmw: Simplify to use mTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS)
2025-11-22 05:24:30 -05:00
crsawyer
f6866f5408 Fixed mbe recording for P25-P2-TS2 (#2272) 2025-11-22 05:20:03 -05:00
crsawyer
d09fad56db P25 P1 MBE ESS order (#2273)
* Modified P25P1 MBE ESS order. ESS will now be populated after LDU2, not at the beginning of LDU2

* Added a version field and incremented it to '2' to signify a breaking change in the order of P25P1 ESS placement.
2025-11-22 05:18:41 -05:00
Denny Sheirer
7c06075880 #2283 Updates usb4java native libraries to 1.3.3 for MacOS Tahoe (26) fixes. (#2308)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-22 04:23:02 -05:00
Denny Sheirer
5b597e29eb #2300 Fixes Motorola unit GPS parsing, fixes P25P1 message viewer issue, and restores map center on selected coordinate in track history. (#2307)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-21 10:22:35 -05:00
Denny Sheirer
feb66831d4 #2283 Mac-OS Tahoe version 26. Updates usb4java with forked library to support multi-versioned native libraries on aarch-64 (#2306)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-21 08:41:28 -05:00
Denny Sheirer
37d47f4219 #2295 JDK25 and Gradle 9.2 and updated 3rd party libraries. (#2296)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-16 08:45:26 -05:00
Denny Sheirer
0d886c272e #1067 P25 Phase 1 C4FM/LSM decoder redesign. (#2294)
C4FM now uses SIMD differential decoding and soft sync detection.
LSM updated with soft sync detector and retains original Gardner error detector.
Adds new QPSK symbol view in the now playing channel tab.
Restores PLL error driven tuner Auto-PPM feature.
Deletes the demod viewer GUI application and all instrumented decoders
Updates DMR to send soft symbols to new symbol view in Now Playing.

Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-11-16 06:52:17 -05:00
Denny Sheirer
48202da9e4 #2287 Resolves Motorola P25P1 LC GPS parsing issue. (#2293)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-11-15 07:35:18 -05:00
Alan Crosswell
c4d9d58a5d Merge pull request #2263 from n2ygk/preserve_geometry
persist preferred spectral and control panel sizes
2025-08-04 03:17:33 -04:00
Denny Sheirer
fe5679fed7 #2265 NBFM noise squelch view saves user applied squelch changes and settings. (#2266)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-08-03 04:17:29 -04:00
Denny Sheirer
b2be38b239 #1310 NBFM decoder uses new Noise Squelch and includes new noise squelch view and control. (#2264)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-08-02 08:34:32 -04:00
Denny Sheirer
912925ee7d 2252 DMR Decoder error where LinearyInterpolator gets mu value outside 0 to 1.0 range. (#2253)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-30 03:21:08 -04:00
Denny Sheirer
d86f8bf3d9 #2250 Logback configuration issue and suppress JDK24 warnings. (#2251)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-28 07:53:27 -04:00
Denny Sheirer
88d8633958 #2234 Streaming Talkgroup Aliasing Feature (#2249)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-28 06:53:55 -04:00
Denny Sheirer
3bf3de82fc #2247 JDK 24 and Gradle 8.14.2. Also updates several 3rd party java libraries. (#2248)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-27 06:19:46 -04:00
Herb Jellinek
03a015f504 #2239 Gradle Javadoc task works correctly (#2240)
Fixes #2239 by adding the jdk.incubator.vector module, ignoring missing tags and malformed HTML.
2025-06-24 04:49:31 -04:00
Denny Sheirer
15b7923a4f #2226 DMR Decoder adds an equalizer to adjust symbol constellation balance/skew and gain to optimize symbol quadrant alignment/mapping. Reduces sync detectors from 3 to 2 and sets minimum objective channel sample rate to 19.2 kHz. (#2245)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-06-24 04:38:10 -04:00
Denny Sheirer
189173e289 #2219 Byte native buffers now use 8192 (vs 2096) for complex buffer sample size so that the heterodyne channel buffer size is large enough to use with the carrier offset processor that reequires 128 sample buffers. (#2220)
Co-authored-by: denny <dennis.sheirer@gmail.com>
2025-04-20 08:25:24 -04:00
Denny Sheirer
2dcd994eb2 #2212 Introduce carrier offset measurement processor to monitor a 12.5 kHz channel and provide estimated carrier offset measurements that allow the tuner's frequency error monitor to correct for the average error reported from each channel. Uses a 15 dB threshold where the signal must be at least 15 dB above the noise floor before it will calculate and broadcast the estimate. Stores the updated PPM in the tuner configuration after each update to ensure the value persists across sessions. When selected in the tuner panel, updates the displayed PPM value after each PPM autocorrection. (#2218)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-04-20 06:55:43 -04:00
Denny Sheirer
afadb227db #2209 Playlist Manager double click selects the playlist (#2210)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-22 07:16:12 -04:00
tadscottsmith
5dbcd5a8ad OpenMHz Broadcast Enhancements (#2207) 2025-03-22 06:26:15 -04:00
Denny Sheirer
728a85c180 #2149 P25P1 adds L3Harris Talker GPS support (#2208)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-22 06:22:40 -04:00
Denny Sheirer
dd6ddd59e9 #2193 Removes "Log Settings" button from the channel view. This is a debug button. (#2205)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 08:02:47 -04:00
Denny Sheirer
758794fdb4 #2195 Fix P25P1 NPE when accessing AMBTC network status message data blocks. (#2204)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 07:54:47 -04:00
Denny Sheirer
49b95a9445 #2202 DMR talker aliases now show in channel activity summary. Added 'TA-' prefix to talker aliases (DMR & P25) to visually highlight what the value represents. (#2203)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 07:27:33 -04:00
Denny Sheirer
a4bcdfa769 #2127 DMR capacity plus wide area voice channel user link control messages now create a call event (#2201)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 06:46:28 -04:00
Denny Sheirer
872498badc #2166 DMR adds RS(12,9) error detection and correction on link control. Enhances DMR CRC mask manager to process CSBK and link control messages to improve handling of RAS enabled systems when the user has not opted to ignore CRC checksums. Fixes issue with CapMax voice channels not resolving to frequency. Fixes issue in playlist editor where DMR LCN description values were not updating correctly. (#2200)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-21 06:23:58 -04:00
Sir Christoff Von-Dryden Of The Bailey LLC
4afb74a847 Check the I2CRepeater state before changing it. (#2197)
Resolves #1923 - 'USB Error 9' preventing configuration of rtl-sdr devices
2025-03-20 02:17:31 -04:00
Denny Sheirer
96f0b7dd55 #2174 Playlist editor channels tab, frequency column sort ordering. (#2192)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-14 04:04:59 -04:00
Randy
b360950b80 Update IntegerFormatter.java for negative squelch value (#2187)
Two issues:
(1)documentation for change.getText() says it can be an empty string but never null. 
(2)Need to allow for typing a minus sign before validating min/max.
2025-03-14 03:52:49 -04:00
Denny Sheirer
5b0a1401a1 #2157 Resolves audio output getting 'stuck' when the thread is blocking on an attempt to write audio to the data line and that blocking call is not executing. Also updates previous attempt at fixing a stalled data line by closing and reopening the line, without having to recreate the data line, which was causing related issues with the gain and muting behavior. (#2191)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-13 04:09:26 -04:00
Daniel Ponte
3fa114501b Add talker alias field to rdio-scanner upload (#2186) 2025-03-11 02:53:51 -04:00
Denny Sheirer
8935835921 #2177 DMR TMS packet parsing issue (#2178)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-10 03:54:02 -04:00
Denny Sheirer
dec255d444 #2175 DMR Text Message Service (TMS) support. (#2176)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-10 03:45:16 -04:00
tadscottsmith
1cb0490985 Add Motorola Talker Alias Identifier (#2154)
* Add MotorolaTalkerAlias identifier.

* Remove duplicate call to talkerAliasManager.

* Use TalkerAlias RID and remove additional cosmetic updates.
2025-03-10 02:27:39 -04:00
Denny Sheirer
c8235ea4fc #2171 Updates OpenJDK to version 23.0.2+9 release for the build system. (#2172)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-03-09 09:00:29 -04:00
Randy
7d69d829b5 Fix adding available channel to wrong queue (#2169)
Available channel was added to P1 queue instead of P2 queue.
2025-03-09 07:40:38 -04:00
Randy
a46abf7da8 Cleanup of recording paths #2128 (#2168)
* Update replaceIllegalCharacters

* Add underscore after timestamp
2025-03-09 07:34:52 -04:00
modificaz
437264e272 Fix incorrect array assignment (frame_3 → frame_2) (#2159) 2025-03-09 07:29:23 -04:00
Denny Sheirer
8718d04ef5 #2162 APCO25 NID BCH decoder implementation. (#2163)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-02-27 09:09:46 -05:00
Denny Sheirer
4e367bb5fe W#2150 Adds AMBE audio tone aliasing support to DMR. (#2151)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-02-15 07:20:46 -05:00
Dennis Sheirer
71d2cb6fca sdrtrunk GitHub issues templates update. 2025-01-20 06:50:59 -05:00
Dennis Sheirer
e856de273d sdrtrunk GitHub issues templates update. 2025-01-20 06:48:04 -05:00
Denny Sheirer
ea0a9da24c #2138 DMR audio segments get chopped with every invalid link control message and remove the insert of Talkgroup 1 as a default value for conventional and direct mode channels. (#2139)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-20 06:40:27 -05:00
Denny Sheirer
b3cd7d787a #2125 Motorola Talker Alias CRC index out of bounds error. (#2126)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-07 03:44:58 -05:00
Denny Sheirer
9a13a82ed1 Update README.md (#2124)
Updates min OSX supported version.
2025-01-04 07:07:43 -05:00
Denny Sheirer
9b71508d49 #2122 Removes extraneous logging from DMR processing. (#2123)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-01 10:05:20 -05:00
Denny Sheirer
694ea83a80 #2120 DMR Decoder resolves index out of bounds error and also updates Hamming 16/17 decoders to correctly identify multi-bit errors when the hamming parity bit is the syndrome. (#2121)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2025-01-01 09:51:41 -05:00
Denny Sheirer
70fb74b787 #2117 DMR simplex transmissions now frame correctly and added additional case for FLC BPTC error correction. (#2118)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-25 08:14:48 -05:00
Denny Sheirer
9ffa02e2e5 #1633 DMR Decoder redesign. Uses new DQPSK vector/scalar demodulator implementations and soft sync detectors for more accurate detections and symbol timing and symbol period calculations. Includes two new calibrations with scalar and vector implementations for DQPSK demodulator and Interpolator. Enhances DMR error detection and correction algorithms, specifically the block product turbo code algorithms. (#2115)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-24 06:53:58 -05:00
ilyacodes
11abb87177 Add Motorola Failsoft LC opcode $02 (#2104)
This is broadcast on channels when a site is in failsoft operation (site controller has failed or too few channels are functional to operate in trunking mode).
2024-12-06 12:29:05 -05:00
Denny Sheirer
49cf92c7ab #2105 P25 P1/2 Motorola Talker Alias CRC Check (#2106)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-06 04:33:14 -05:00
Denny Sheirer
4cf51405ea #2102 Log Unknown Tuners From SDRPlay API (#2102)
Co-authored-by: Dennis Sheirer <dsheirer@github.com>
2024-12-03 17:16:37 -05:00
1167 changed files with 85445 additions and 20810 deletions

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Support and Questions - How do I XXX? Does it support XXX?
url: https://github.com/DSheirer/sdrtrunk/wiki/Support
about: Please ask your user questions here -- DO NOT create an issue if you have a user question please!!

View File

@@ -1,16 +0,0 @@
---
name: How-To Question or Does The Software Support xxx
about: End user question or support discussion
title: STOP - Don't Use Issue Reporting to Post End-User Questions!!!
labels: ''
assignees: ''
---
STOP !!!!
GitHub issues are for reporting software bugs or requesting enhancements.
If you have a question about how to use the software, or if you want to know if sdrtrunk supports a protocol, tuner, or other aspect, please post your question in one of the Support forums.
Visit: https://github.com/DSheirer/sdrtrunk/wiki

View File

@@ -22,10 +22,10 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 23
- name: Set up JDK 27
uses: actions/setup-java@v3
with:
java-version: '23'
java-version: '27'
distribution: 'liberica'
java-package: 'jdk+fx'
- name: Build with Gradle

View File

@@ -15,10 +15,11 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up JDK 23
- name: Set up JDK 27
uses: actions/setup-java@v3
with:
java-version: '23'
# Must match the JDK version downloaded in build.gradle (jdk_download_base) - jlink requires an exact version match
java-version: '27.0.0'
distribution: 'liberica'
java-package: 'jdk+fx'
@@ -37,11 +38,12 @@ jobs:
echo -e "ref: $GITHUB_REF \ncommit: $GITHUB_SHA\nbuild: $(date +"%Y-%m-%dT%H:%M:%SZ")" > build/image/build_info.txt
- name: Add build artifacts to nightly release
uses: pyTooling/Actions/releaser@main
uses: softprops/action-gh-release@v2
with:
tag: nightly
rm: false
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: nightly
prerelease: true
files: |
build/image/*.zip
build/image/build_info.txt
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -2,18 +2,24 @@
<dictionary name="denny">
<words>
<w>bahroo</w>
<w>baseband</w>
<w>basebanded</w>
<w>broadcastify</w>
<w>cach</w>
<w>correlator</w>
<w>deinterleave</w>
<w>descrambled</w>
<w>dibit</w>
<w>dinf</w>
<w>dqpsk</w>
<w>esnid</w>
<w>fleetsync</w>
<w>heterdyned</w>
<w>heterodynes</w>
<w>hytera</w>
<w>icecast</w>
<w>ident</w>
<w>lcns</w>
<w>nutall</w>
<w>overflowable</w>
<w>polyphase</w>

45
.idea/misc.xml generated
View File

@@ -4,7 +4,50 @@
<component name="FrameworkDetectionExcludesConfiguration">
<file type="web" url="file://$PROJECT_DIR$" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23_PREVIEW" project-jdk-name="liberica-23" project-jdk-type="JavaSDK">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
<option name="myDefaultNotNull" value="org.jetbrains.annotations.NotNull" />
<option name="myOrdered" value="true" />
<option name="myNullables">
<value>
<list size="13">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="8" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.Nullable" />
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.Nullable" />
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.Nullable" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="13">
<item index="0" class="java.lang.String" itemvalue="org.jspecify.annotations.NonNull" />
<item index="1" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="2" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="3" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="4" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="5" class="java.lang.String" itemvalue="org.eclipse.jdt.annotation.NonNull" />
<item index="6" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="7" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="8" class="java.lang.String" itemvalue="lombok.NonNull" />
<item index="9" class="java.lang.String" itemvalue="io.reactivex.annotations.NonNull" />
<item index="10" class="java.lang.String" itemvalue="io.reactivex.rxjava3.annotations.NonNull" />
<item index="11" class="java.lang.String" itemvalue="org.springframework.lang.NonNull" />
<item index="12" class="java.lang.String" itemvalue="jakarta.annotation.Nonnull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_27_PREVIEW" project-jdk-name="liberica-27" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,6 +1,24 @@
![Gradle Build](https://github.com/dsheirer/sdrtrunk/actions/workflows/gradle.yml/badge.svg)
![Nightly Release](https://github.com/dsheirer/sdrtrunk/actions/workflows/nightly.yml/badge.svg)
# MacOS Tahoe 26.1 Users - Attention:
Changes to USB support in Tahoe version 26.x cause sdrtrunk to fail to launch. Do the following to install the latest libusb and create a symbolic link and then use the nightly build which includes an updated usb4java native library for Tahoe with ARM processor. There may still be issue(s) with MacOS accessing your USB SDR tuners.
```
brew install libusb --HEAD
cd /opt
sudo mkdir local
cd local
sudo mkdir lib
```
Next, find where brew installed the libusb library, for example: ```/opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib``` Note: the folder "HEAD-9ceaa52" is the version stamp for HEAD when you installed from it.
Finally, create a symbolic link from the installed library to the place where usb4java is expecting to find libusb (/opt/local/lib/libusb-1.0.0.dylib)
```
sudo ln -s /opt/homebrew/Cellar/libusb/HEAD-9ceaa52/lib/libusb-1.0.0.dylib /opt/local/lib/libusb-1.0.0.dylib
```
# sdrtrunk
A cross-platform java application for decoding, monitoring, recording and streaming trunked mobile and related radio protocols using Software Defined Radios (SDR).
@@ -28,6 +46,6 @@ playlist(s) before you use the nightly builds.** Note: the nightly release is u
committed to the code base, so it's not really 'nightly' as much as it is 'current'.
## Minimum System Requirements
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac/Linux (64-bit, 10.14 or higher)
* **Operating System:** Windows (~~32 or~~ 64-bit), Linux (~~32 or~~ 64-bit) or Mac (64-bit, 12.x or higher)
* **CPU:** 4-core
* **RAM:** 8GB or more (preferred). Depending on usage, 4GB may be sufficient.

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,16 +17,18 @@
* ****************************************************************************
*/
import org.beryx.runtime.RuntimeZipTask
import org.gradle.internal.jvm.Jvm
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
import java.text.SimpleDateFormat
/**
* Instructions for building/compiling the sdrtrunk application.
*
* Prerequisites:
* Install and configure an OpenJDK version 22+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
* - Optional: install and configure Gradle 8.10+
* Install and configure an OpenJDK version 27+ that includes the JavaFX modules (e.g. Bellsoft Liberica JDK)
* - Optional: install and configure Gradle 9.8+
*
* Scenario 1: run the application via gradle command line from the source code root directory:
* command: ./gradlew run
@@ -47,27 +49,43 @@ plugins {
id 'application'
id 'java'
id 'idea'
id 'org.beryx.runtime' version '1.12.7'
}
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
id 'org.beryx.runtime' version '2.0.1'
}
//Version is defined in gradle.properties so that we can CLI override it for nightly builds.
version = project.findProperty('projectVersion')
//Java 23 is required for this version of the Project Panama preview/incubator feature
//Java 27 is required for this version of the Project Panama preview/incubator feature
java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
languageVersion = JavaLanguageVersion.of(27)
vendor = JvmVendorSpec.BELLSOFT
}
}
sourceSets {
main.java.srcDirs 'src/main'
test.java.srcDirs 'src/test'
/**
* Verifies that the selected toolchain JDK includes the JavaFX modules (e.g. Bellsoft Liberica 'Full JDK').
*
* Gradle's toolchain spec can only select by language version, vendor and implementation, so it can't distinguish
* between the Liberica JDK packages that do and don't bundle JavaFX. This task fails the build with a clear message
* instead of allowing compilation to fail with missing javafx packages.
*/
tasks.register('verifyToolchainJavaFx') {
def jdkHome = javaToolchains.launcherFor(java.toolchain).map { it.metadata.installationPath.asFile }
doLast {
def release = new File(jdkHome.get(), 'release')
if(!release.exists() || !release.text.contains('javafx.base')) {
throw new GradleException("The JDK at [${jdkHome.get()}] does not include the JavaFX modules. Install " +
"the Bellsoft Liberica JDK 27 'Full' package (includes LibericaFX), or use " +
"-Porg.gradle.java.installations.paths=<jdk path> to specify the JDK that Gradle should use.")
}
}
}
tasks.named('compileJava') {
dependsOn 'verifyToolchainJavaFx'
}
test {
@@ -77,58 +95,62 @@ test {
dependencies {
// JUnit Tests
testImplementation(platform("org.junit:junit-bom:5.11.0"))
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.0'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine")
testImplementation(platform("org.junit:junit-bom:6.0.1"))
testImplementation 'org.junit.jupiter:junit-jupiter-api:6.0.1'
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:6.0.1")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:6.0.1")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
//Jitpack imports
implementation 'com.github.dnault:libresample4j:master'
implementation 'com.github.DSheirer.jmbe:jmbe-api:1.0.0'
implementation 'com.github.DSheirer.jmbe:api:v2.1.0'
implementation 'com.github.nwaldispuehl:java-lame:v3.98.4'
//mavenCentral/jcenter imports
implementation 'ch.qos.logback:logback-classic:1.4.12'
implementation 'ch.qos.logback:logback-core:1.4.14'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0'
implementation 'com.fazecast:jSerialComm:2.9.3'
implementation 'ch.qos.logback:logback-classic:1.6.3'
implementation 'ch.qos.logback:logback-core:1.5.38'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.22.4'
implementation 'com.fazecast:jSerialComm:2.11.4'
implementation 'com.formdev:flatlaf:3.7.2'
implementation 'com.formdev:flatlaf-intellij-themes:3.7.2'
implementation 'com.github.jiconfont:jiconfont-font_awesome:4.7.0.1'
implementation 'com.github.jiconfont:jiconfont-javafx:1.0.0'
implementation 'com.github.jiconfont:jiconfont-swing:1.0.1'
implementation 'com.github.wendykierp:JTransforms:3.1'
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.google.guava:guava:33.3.0-jre'
implementation 'com.github.wendykierp:JTransforms:3.2'
implementation 'com.google.code.gson:gson:2.14.0'
implementation 'com.google.guava:guava:33.6.0-jre'
implementation 'com.jidesoft:jide-oss:3.6.18'
implementation 'com.miglayout:miglayout-swing:11.0'
implementation 'com.miglayout:miglayout-swing:11.4.3'
implementation 'com.mpatric:mp3agic:0.9.1'
implementation 'commons-io:commons-io:2.11.0'
implementation 'commons-io:commons-io:2.22.0'
implementation 'eu.hansolo:charts:1.0.5'
implementation 'io.github.dsheirer:jbladerf:2.6.0' //bladeRF tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:jhydrasdr:1.1.3' //HydraSDR tuners - GitHub release, see settings.gradle
implementation 'io.github.dsheirer:radio-reference-api:18.0.0'
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.1' //OSX & Windows aarch64 native libs
implementation 'io.github.dsheirer:usb4java-native-libraries:1.3.3' //OSX & Windows aarch64 native libs
implementation 'io.github.dsheirer:usb4java:1.3.5' //Fork with Loader support for OSX Multi-Version
implementation 'javax.usb:usb-api:1.0.2'
implementation 'net.coderazzi:tablefilter-swing:5.5.4'
implementation 'org.apache.commons:commons-compress:1.26.0'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-compress:1.28.0'
implementation 'org.apache.commons:commons-lang3:3.20.0'
implementation 'org.apache.commons:commons-math3:3.6.1'
implementation 'org.apache.commons:commons-csv:1.9.0'
implementation 'org.apache.mina:mina-core:2.2.1'
implementation 'org.apache.mina:mina-http:2.2.1'
implementation 'org.controlsfx:controlsfx:11.1.2'
implementation 'org.apache.commons:commons-csv:1.14.1'
implementation 'org.apache.mina:mina-core:2.2.9'
implementation 'org.apache.mina:mina-http:2.2.9'
implementation 'org.controlsfx:controlsfx:11.2.3'
implementation 'org.rauschig:jarchivelib:1.2.0'
implementation 'org.slf4j:slf4j-api:2.0.5'
implementation 'org.slf4j:slf4j-api:2.0.17'
implementation 'org.usb4java:libusb4java:1.3.0'
implementation 'org.usb4java:usb4java:1.3.0'
implementation 'org.usb4java:usb4java-javax:1.3.0'
implementation 'pl.edu.icm:JLargeArrays:1.6'
}
def os = org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem
def os = DefaultNativePlatform.currentOperatingSystem
/**
* This is needed for the JDK23 vector API ... until it moves out of incubation or preview
* This is needed for the JDK23+ vector API ... until it moves out of incubation or preview
*/
tasks.withType(JavaCompile) {
tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add("--enable-preview") //Project Panama Vector API is still in incubator
options.compilerArgs.add("--add-modules=jdk.incubator.vector") //Project Panama Vector API is still in incubator
}
@@ -139,9 +161,11 @@ tasks.withType(JavaCompile) {
def jvmArgsWindows =
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
'--add-exports=java.desktop/com.sun.java.swing.plaf.windows=ALL-UNNAMED', //windows users for JIDE
'--add-modules=jdk.incubator.vector', //Project Panama Vector API is still in incubator
'--add-modules=jdk.incubator.vector', //Project Panama vector API is still in incubator
'--enable-preview', //Project Panama Vector API is still in incubator
'--enable-native-access=ALL-UNNAMED', //Suppress unsafe warnings for FFM API Foreign Memory calls
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
'-Djava.library.path=c:\\Program Files\\SDRplay\\API\\x64'] //SDRPlay API library path
/**
@@ -149,12 +173,14 @@ def jvmArgsWindows =
*/
def jvmArgsLinux =
['--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED', //Needed for controls-fx.jar
'--add-modules=jdk.incubator.vector', //Needed while Project Panama API is still in incubator
'--add-modules=jdk.incubator.vector', //Project Panama vector API is still in incubator
'--enable-preview', //Project Panama Vector & Foreign Function APIs remain in incubator
'--sun-misc-unsafe-memory-access=allow', //JDK24 - suppress sun.misc.Unsafe::allocateMemory warning
'--enable-native-access=javafx.graphics', //JDK24 - suppress restricted java.lang.System calls
'--enable-native-access=ALL-UNNAMED'] //SDRPlay API library path
application {
mainClassName = "io.github.dsheirer.gui.SDRTrunk"
mainClass = "io.github.dsheirer.gui.SDRTrunk"
if(os.isWindows()) {
applicationDefaultJvmArgs = jvmArgsWindows
@@ -164,6 +190,15 @@ application {
}
}
test {
if(os.isWindows()) {
jvmArgs = jvmArgsWindows
}
else {
jvmArgs = jvmArgsLinux
}
}
jar {
manifest {
attributes (
@@ -184,6 +219,11 @@ idea {
}
}
javadoc {
options.addStringOption('-add-modules', 'jdk.incubator.vector')
options.addStringOption('Xdoclint:none', '-quiet')
}
/**
* Runtime target OS and CPU image names. We append version here so that version details are at end of zip filename.
*/
@@ -199,17 +239,17 @@ String targetWindowsAarch64 = 'windows-aarch64-v' + version
* author's development environment, but can be changed to your environment if you want to use this build pattern.
*/
String jdk_base = '/home/denny/java_jdks/'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-22.0.2-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-22.0.2-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-22.0.2-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-22.0.2-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-22.0.2-full'
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-22.0.2-full'
String jdk_linux_aarch64 = jdk_base + 'linux-arm64/jdk-25.0.1-full'
String jdk_linux_x86_64 = jdk_base + 'linux-x64/jdk-25.0.1-full'
String jdk_osx_x86_64 = jdk_base + 'osx-x64/jdk-25.0.1-full.jdk'
String jdk_osx_aarch64 = jdk_base + 'osx-arm64/jdk-25.0.1-full.jdk'
String jdk_windows_x86_64 = jdk_base + 'windows-x64/jdk-25.0.1-full'
String jdk_windows_aarch64 = jdk_base + 'windows-arm64/jdk-25.0.1-full'
/**
* Download URLs to download the JDK as part of the gradle build packaging process
*/
def jdk_download_base = "https://download.bell-sw.com/java/23.0.1+13/bellsoft-jdk23.0.1+13-"
def jdk_download_base = "https://download.bell-sw.com/java/27+36/bellsoft-jdk27+36-"
def jdk_download_suffix = "-full.tar.gz"
def jdk_download_linux_aarch64 = jdk_download_base + "linux-aarch64" + jdk_download_suffix
def jdk_download_linux_x86_64 = jdk_download_base + "linux-amd64" + jdk_download_suffix
@@ -224,16 +264,17 @@ def jdk_download_windows_aarch64 = jdk_download_base + "windows-aarch64-full.zip
* @param jvmArgs to use (windows or linux)
* @return configured task.
*/
def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
def configure(RuntimeZipTask rt, List<String> jvmArgs) {
//Use the correct set of JVM arguments
rt.extension.launcherData.get().jvmArgs = jvmArgs
//Additional modules to include with auto-detected modules.
//jdk.charsets - NXDN BIG5 encoding support
//jdk.crypto.ec - needed for HTTPS connections (broadcastify calls & map tile server)
//jdk.incubator.vector - needed for Project Panama foreign function and vector apis
//jdk.accessibility is used with assistive technologies like screen readers
//java.management for JVM resource monitoring
rt.extension.addModules('jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
rt.extension.addModules('jdk.charsets', 'jdk.crypto.ec', 'jdk.incubator.vector', 'jdk.accessibility', 'java.management')
//Use auto-detected modules and 'add' any specified modules.
rt.extension.additive.set(true)
@@ -242,7 +283,8 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
rt.extension.addOptions('--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages')
//Runtime images have auto appended version in image zip name. Override to include version in single target image.
rt.extension.imageZip.set(file("$buildDir/image/sdr-trunk.zip"))
// rt.extension.imageDir=layout.buildDirectory.dir('image')
rt.extension.imageZip.set(file(layout.buildDirectory.dir('image').get().file('sdr-trunk.zip')))
}
/**
@@ -254,35 +296,35 @@ def configure(org.beryx.runtime.RuntimeZipTask rt, List<String> jvmArgs) {
* Usage: gradlew runtimeZipCurrent (windows - using gradlew)
* Usage: ./gradlew runtimeZipCurrent (linux - using gradlew)
*/
tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
tasks.register('runtimeZipCurrent', RuntimeZipTask) {rt ->
def arch = System.properties.getProperty("os.arch")
def javaHome = org.gradle.internal.jvm.Jvm.current().getJavaHome().toString()
def javaHome = Jvm.current().getJavaHome().toString()
if(os.isWindows()) {
if(arch.equals("amd64")) {
if(arch == "amd64") {
rt.extension.targetPlatform(targetWindowsX86_64, javaHome)
}
else if(arch.equals("aarch64")) {
else if(arch == "aarch64") {
rt.extension.targetPlatform(targetWindowsAarch64, javaHome)
}
configure(rt, jvmArgsWindows)
}
else if(os.isLinux())
{
if(arch.equals("amd64")) {
if(arch == "amd64") {
rt.extension.targetPlatform(targetLinuxX86_64, javaHome)
}
else if(arch.equals("aarch64")) {
else if(arch == "aarch64") {
rt.extension.targetPlatform(targetLinuxAarch64, javaHome)
}
configure(rt, jvmArgsLinux)
}
else if(os.isMacOsX())
{
if(arch.equals("amd64")) {
if(arch == "amd64") {
rt.extension.targetPlatform(targetOsxX86_64, javaHome)
}
else if(arch.equals("aarch64")) {
else if(arch == "aarch64") {
rt.extension.targetPlatform(targetOsxAarch64, javaHome)
}
configure(rt, jvmArgsLinux)
@@ -299,7 +341,7 @@ tasks.register('runtimeZipCurrent', org.beryx.runtime.RuntimeZipTask) {rt ->
* Usage: gradlew runtimeZipWindows (windows - using gradlew)
* Usage: ./gradlew runtimeZipWindows (linux - using gradlew)
*/
tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
tasks.register('runtimeZipWindows', RuntimeZipTask) {rt ->
//Windows aarch64
if(file(jdk_windows_aarch64).exists()) {
rt.extension.targetPlatform(targetWindowsAarch64, jdk_windows_aarch64, [])
@@ -331,7 +373,7 @@ tasks.register('runtimeZipWindows', org.beryx.runtime.RuntimeZipTask) {rt ->
* Usage: gradlew runtimeZipOthers (windows - using gradlew)
* Usage: ./gradlew runtimeZipOthers (linux - using gradlew)
*/
tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
tasks.register('runtimeZipOthers', RuntimeZipTask) {rt ->
//Linux aarch64
if(file(jdk_linux_aarch64).exists()) {
rt.extension.targetPlatform(targetLinuxAarch64, jdk_linux_aarch64, [])
@@ -369,5 +411,5 @@ tasks.register('runtimeZipOthers', org.beryx.runtime.RuntimeZipTask) {rt ->
}
}
configure(rt, jvmArgsLinux);
configure(rt, jvmArgsLinux)
}

View File

@@ -1,6 +1,6 @@
#
# ******************************************************************************
# Copyright (C) 2014-2024 Dennis Sheirer
# Copyright (C) 2014-2026 Dennis Sheirer
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -28,4 +28,4 @@
# tag name, specifically the dashes inserted before and after the alpha/beta labels.
# See: https://github.com/DSheirer/sdrtrunk/issues/1651
projectVersion=0.6.1
projectVersion=0.6.2-beta-1

Binary file not shown.

View File

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

53
gradlew vendored
View File

@@ -1,27 +1,26 @@
#!/bin/sh
#
# ******************************************************************************
# Copyright (C) 2014-2024 Dennis Sheirer
# Copyright © 2015 the original authors.
#
# This 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.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# This program is 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.
# https://www.apache.org/licenses/LICENSE-2.0
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
# *****************************************************************************
# 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.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -30,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -58,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -86,7 +85,8 @@ done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@@ -147,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
@@ -155,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -204,16 +202,15 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.

58
gradlew.bat vendored
View File

@@ -13,16 +13,18 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -43,13 +45,13 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -57,36 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%

View File

@@ -1 +1,28 @@
rootProject.name = 'sdr-trunk'
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'sdr-trunk'
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
//jHydraSDR is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jHydraSDR/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jhydrasdr' }
}
//jBladeRF is published as a GitHub release, with the native libraries for all platforms in the jar
ivy {
url = 'https://github.com/DSheirer/jBladeRF/releases/download'
patternLayout { artifact 'v[revision]/[artifact]-[revision].[ext]' }
metadataSources { artifact() }
content { includeModule 'io.github.dsheirer', 'jbladerf' }
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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 com.sun.java.swing.plaf.windows;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
/**
* No-op stub for the JDK's Windows look-and-feel.
*
* <p>JIDE 3.6.18's {@code LookAndFeelFactory} contains compile-time
* {@code instanceof com.sun.java.swing.plaf.windows.WindowsLookAndFeel} checks throughout its
* dispatch logic. On Linux and macOS JDK builds that class is genuinely absent from
* {@code java.desktop}, so the JVM throws {@link NoClassDefFoundError} the first time JIDE's
* dispatch code executes the {@code instanceof} bytecode. That happens during
* {@link com.jidesoft.swing.JideSplitPane#updateUI()} on every LAF change.
*
* <p>This stub lives in the classpath so the class reference resolves on non-Windows JDKs.
* On Windows JDK builds the real {@code WindowsLookAndFeel} is loaded from the
* {@code java.desktop} module via the boot class loader and shadows this class.
*
* <p>The stub reports itself as not native and not supported - JIDE never instantiates this
* class, it only uses it for {@code instanceof} comparisons against the active LAF. Since
* sdrtrunk never installs this LAF, the {@code instanceof} check always returns false and
* JIDE proceeds to its next branch.
*/
public class WindowsLookAndFeel extends LookAndFeel
{
@Override
public String getName()
{
return "Windows (stub)";
}
@Override
public String getID()
{
return "WindowsStub";
}
@Override
public String getDescription()
{
return "Compatibility stub for JIDE's instanceof checks on non-Windows JDKs.";
}
@Override
public boolean isNativeLookAndFeel()
{
return false;
}
@Override
public boolean isSupportedLookAndFeel()
{
return false;
}
@Override
public UIDefaults getDefaults()
{
return new UIDefaults();
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias;
@@ -30,13 +27,22 @@ import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.broadcast.BroadcastChannel;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.record.Record;
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
@@ -46,13 +52,6 @@ import javafx.util.Callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* Alias provides an aliasing (e.g. name, color, etc) container that is linked to multiple alias identifiers and
* provides a corresponding set of actions related to the alias.
@@ -74,6 +73,7 @@ public class Alias
private StringProperty mName = new SimpleStringProperty();
private ObservableList<AliasID> mAliasIDs = FXCollections.observableArrayList();
private ObservableList<AliasAction> mAliasActions = FXCollections.observableArrayList();
private ObjectProperty<StreamAsTalkgroup> mStreamTalkgroupAlias = new SimpleObjectProperty<>();
/**
* Constructs an instance and sets the specified name.
@@ -180,6 +180,12 @@ public class Alias
return mColor;
}
@JsonIgnore
public ObjectProperty streamTalkgroupAliasProperty()
{
return mStreamTalkgroupAlias;
}
/**
* Icon name property
*/
@@ -229,6 +235,24 @@ public class Alias
mName.set(name);
}
/**
* Alias name
*/
@JacksonXmlProperty(isAttribute = true, localName = "stream_talkgroup_alias")
public StreamAsTalkgroup getStreamTalkgroupAlias()
{
return mStreamTalkgroupAlias.get();
}
/**
* Sets the stream as talkgroup value that will be used in the TO field for streamed audio calls.
* @param streamTalkgroupAlias with a talkgroup value.
*/
public void setStreamTalkgroupAlias(StreamAsTalkgroup streamTalkgroupAlias)
{
mStreamTalkgroupAlias.set(streamTalkgroupAlias);
}
/**
* Alias list name for the alias list that this alias belongs to
*/
@@ -710,6 +734,7 @@ public class Alias
{
return (Alias a) -> new Observable[] {a.recordableProperty(), a.streamableProperty(), a.colorProperty(),
a.aliasListNameProperty(), a.groupProperty(), a.iconNameProperty(), a.nameProperty(), a.aliasIds(),
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty()};
a.aliasActions(), a.nonAudioIdentifierCountProperty(), a.overlapProperty(), a.priorityProperty(),
a.streamTalkgroupAliasProperty()};
}
}

View File

@@ -30,12 +30,16 @@ import io.github.dsheirer.alias.id.legacy.mobileID.Min;
import io.github.dsheirer.alias.id.legacy.siteID.SiteID;
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio;
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange;
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
import io.github.dsheirer.alias.id.radio.Radio;
import io.github.dsheirer.alias.id.radio.RadioRange;
import io.github.dsheirer.alias.id.record.Record;
import io.github.dsheirer.alias.id.status.UnitStatusID;
import io.github.dsheirer.alias.id.status.UserStatusID;
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange;
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
@@ -79,6 +83,31 @@ public class AliasFactory
Min copyMin = new Min();
copyMin.setMin(originalMin.getMin());
return copyMin;
case NXDN_TYPE_D_RADIO_ID:
NXDNTypeDRadio originalTypeDRadio = (NXDNTypeDRadio)id;
NXDNTypeDRadio copyTypeDRadio = new NXDNTypeDRadio(originalTypeDRadio.getHomeRepeater(),
originalTypeDRadio.getIdentifier());
copyTypeDRadio.setOverlap(originalTypeDRadio.overlapProperty().get());
return copyTypeDRadio;
case NXDN_TYPE_D_RADIO_ID_RANGE:
NXDNTypeDRadioRange originalTypeDRadioRange = (NXDNTypeDRadioRange)id;
NXDNTypeDRadioRange copyTypeDRadioRange = new NXDNTypeDRadioRange(originalTypeDRadioRange.getHomeRepeater(),
originalTypeDRadioRange.getMinIdentifier(), originalTypeDRadioRange.getMaxIdentifier());
copyTypeDRadioRange.setOverlap(originalTypeDRadioRange.overlapProperty().get());
return copyTypeDRadioRange;
case NXDN_TYPE_D_TALKGROUP:
NXDNTypeDTalkgroup originalTypeDTalkgroup = (NXDNTypeDTalkgroup)id;
NXDNTypeDTalkgroup copyTypeDTalkgroup = new NXDNTypeDTalkgroup(originalTypeDTalkgroup.getHomeRepeater(),
originalTypeDTalkgroup.getIdentifier());
copyTypeDTalkgroup.setOverlap(originalTypeDTalkgroup.overlapProperty().get());
return copyTypeDTalkgroup;
case NXDN_TYPE_D_TALKGROUP_RANGE:
NXDNTypeDTalkgroupRange originalTypeDTalkgroupRange = (NXDNTypeDTalkgroupRange)id;
NXDNTypeDTalkgroupRange copyTypeDTalkgroupRange = new NXDNTypeDTalkgroupRange(
originalTypeDTalkgroupRange.getHomeRepeater(), originalTypeDTalkgroupRange.getMinIdentifier(),
originalTypeDTalkgroupRange.getMaxIdentifier());
copyTypeDTalkgroupRange.setOverlap(originalTypeDTalkgroupRange.overlapProperty().get());
return copyTypeDTalkgroupRange;
case P25_FULLY_QUALIFIED_RADIO_ID:
P25FullyQualifiedRadio originalP25 = (P25FullyQualifiedRadio) id;
P25FullyQualifiedRadio copyP25 = new P25FullyQualifiedRadio(originalP25.getWacn(),

View File

@@ -47,7 +47,7 @@ import io.github.dsheirer.identifier.talkgroup.FullyQualifiedTalkgroupIdentifier
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.identifier.tone.ToneIdentifier;
import io.github.dsheirer.identifier.tone.ToneSequence;
import io.github.dsheirer.module.decode.dcs.DCSCode;
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
import io.github.dsheirer.protocol.Protocol;
import java.util.ArrayList;
import java.util.Collection;
@@ -134,6 +134,7 @@ public class AliasList
switch(id.getType())
{
case TALKGROUP:
case NXDN_TYPE_D_TALKGROUP:
Talkgroup talkgroup = (Talkgroup)id;
TalkgroupAliasList talkgroupAliasList = mTalkgroupProtocolMap.get(talkgroup.getProtocol());
@@ -147,6 +148,7 @@ public class AliasList
talkgroupAliasList.add(talkgroup, alias);
break;
case TALKGROUP_RANGE:
case NXDN_TYPE_D_TALKGROUP_RANGE:
TalkgroupRange talkgroupRange = (TalkgroupRange)id;
TalkgroupAliasList talkgroupRangeAliasList = mTalkgroupProtocolMap.get(talkgroupRange.getProtocol());
@@ -186,6 +188,7 @@ public class AliasList
p25TalkgroupAliasList.add(qualifiedTalkgroup, alias);
break;
case RADIO_ID:
case NXDN_TYPE_D_RADIO_ID:
Radio radio = (Radio)id;
RadioAliasList radioAliasList = mRadioProtocolMap.get(radio.getProtocol());
@@ -199,6 +202,7 @@ public class AliasList
radioAliasList.add(radio, alias);
break;
case RADIO_ID_RANGE:
case NXDN_TYPE_D_RADIO_ID_RANGE:
RadioRange radioRange = (RadioRange)id;
RadioAliasList radioRangeAliasList = mRadioProtocolMap.get(radioRange.getProtocol());

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -35,13 +35,18 @@ import io.github.dsheirer.alias.id.legacy.talkgroup.LegacyTalkgroupID;
import io.github.dsheirer.alias.id.legacy.uniqueID.UniqueID;
import io.github.dsheirer.alias.id.lojack.LoJackFunctionAndID;
import io.github.dsheirer.alias.id.priority.Priority;
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadio;
import io.github.dsheirer.alias.id.radio.NXDNTypeDRadioRange;
import io.github.dsheirer.alias.id.radio.P25FullyQualifiedRadio;
import io.github.dsheirer.alias.id.radio.Radio;
import io.github.dsheirer.alias.id.radio.RadioRange;
import io.github.dsheirer.alias.id.record.Record;
import io.github.dsheirer.alias.id.status.UnitStatusID;
import io.github.dsheirer.alias.id.status.UserStatusID;
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.NXDNTypeDTalkgroupRange;
import io.github.dsheirer.alias.id.talkgroup.P25FullyQualifiedTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.StreamAsTalkgroup;
import io.github.dsheirer.alias.id.talkgroup.Talkgroup;
import io.github.dsheirer.alias.id.talkgroup.TalkgroupRange;
import io.github.dsheirer.alias.id.tone.TonesID;
@@ -63,6 +68,10 @@ import javafx.util.Callback;
@JsonSubTypes.Type(value = Min.class, name = "min"),
@JsonSubTypes.Type(value = MPT1327ID.class, name = "mpt1327ID"),
@JsonSubTypes.Type(value = NonRecordable.class, name = "nonRecordable"),
@JsonSubTypes.Type(value = NXDNTypeDRadio.class, name = "nxdnTypeDRadio"),
@JsonSubTypes.Type(value = NXDNTypeDRadioRange.class, name = "nxdnTypeDRadioRange"),
@JsonSubTypes.Type(value = NXDNTypeDTalkgroup.class, name = "nxdnTypeDTalkgroup"),
@JsonSubTypes.Type(value = NXDNTypeDTalkgroupRange.class, name = "nxdnTypeDTalkgroupRange"),
@JsonSubTypes.Type(value = P25FullyQualifiedRadio.class, name = "p25FullyQualifiedRadio"),
@JsonSubTypes.Type(value = P25FullyQualifiedTalkgroup.class, name = "p25FullyQualifiedTalkgroup"),
@JsonSubTypes.Type(value = Priority.class, name = "priority"),
@@ -70,6 +79,7 @@ import javafx.util.Callback;
@JsonSubTypes.Type(value = RadioRange.class, name = "radioRange"),
@JsonSubTypes.Type(value = Record.class, name = "record"),
@JsonSubTypes.Type(value = SiteID.class, name = "siteID"),
@JsonSubTypes.Type(value = StreamAsTalkgroup.class, name = "streamAsTalkgroup"),
@JsonSubTypes.Type(value = Talkgroup.class, name = "talkgroup"),
@JsonSubTypes.Type(value = TalkgroupRange.class, name = "talkgroupRange"),
@JsonSubTypes.Type(value = TonesID.class, name = "tones"),

View File

@@ -29,6 +29,10 @@ public enum AliasIDType
LOJACK("LoJack"),
LTR_NET_UID("LTR-Net UID"),
MIN("Passport MIN"),
NXDN_TYPE_D_RADIO_ID("NXDN Type-D Radio ID"),
NXDN_TYPE_D_RADIO_ID_RANGE("NXDN Type-D Radio ID Range"),
NXDN_TYPE_D_TALKGROUP("NXDN Type-D Talkgroup"),
NXDN_TYPE_D_TALKGROUP_RANGE("NXDN Type-D Talkgroup Range"),
P25_FULLY_QUALIFIED_RADIO_ID("P25 Fully Qualified Radio ID"),
P25_FULLY_QUALIFIED_TALKGROUP("P25 Fully Qualified Talkgroup"),
PRIORITY("Audio Priority"),
@@ -58,6 +62,7 @@ public enum AliasIDType
//Values used by the View-By alias editor
public static EnumSet<AliasIDType> VIEW_BY_VALUES = EnumSet.of(TALKGROUP, TALKGROUP_RANGE, RADIO_ID, RADIO_ID_RANGE,
NXDN_TYPE_D_RADIO_ID, NXDN_TYPE_D_RADIO_ID_RANGE, NXDN_TYPE_D_TALKGROUP, NXDN_TYPE_D_TALKGROUP_RANGE,
P25_FULLY_QUALIFIED_RADIO_ID, P25_FULLY_QUALIFIED_TALKGROUP, UNIT_STATUS, STATUS, TONES);
public String toString()

View File

@@ -0,0 +1,46 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id;
/**
* NXDN Type-D alias identifier composed of a home repeater and a radio or talkgroup identifier value.
*/
public interface INXDNTypeD
{
/**
* Home repeater for the identifier
*/
int getHomeRepeater();
/**
* Sets the home repeater for the identifier
*/
void setHomeRepeater(int homeRepeater);
/**
* Radio or talkgroup value, without the home repeater.
*/
int getIdentifier();
/**
* Sets the radio or talkgroup value, without the home repeater.
*/
void setIdentifier(int identifier);
}

View File

@@ -0,0 +1,56 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id;
/**
* NXDN Type-D alias identifier range of radio or talkgroup values that share a common home repeater.
*/
public interface INXDNTypeDRange
{
/**
* Home repeater for the range
*/
int getHomeRepeater();
/**
* Sets the home repeater for the range
*/
void setHomeRepeater(int homeRepeater);
/**
* Minimum radio or talkgroup value, without the home repeater.
*/
int getMinIdentifier();
/**
* Sets the minimum radio or talkgroup value, without the home repeater.
*/
void setMinIdentifier(int identifier);
/**
* Maximum radio or talkgroup value, without the home repeater.
*/
int getMaxIdentifier();
/**
* Sets the maximum radio or talkgroup value, without the home repeater.
*/
void setMaxIdentifier(int identifier);
}

View File

@@ -22,7 +22,7 @@ package io.github.dsheirer.alias.id.dcs;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import io.github.dsheirer.alias.id.AliasID;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.module.decode.dcs.DCSCode;
import io.github.dsheirer.module.decode.squelch.dcs.DCSCode;
/**
* Digital Coded Squelch (DCS) tone identifier

View File

@@ -0,0 +1,96 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.radio;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.INXDNTypeD;
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN Type-D radio. The value is the over-the-air 16-bit identifier that combines the home repeater and radio so that
* it matches the value of an NXDN Type-D radio identifier.
*/
public class NXDNTypeDRadio extends Radio implements INXDNTypeD
{
public NXDNTypeDRadio()
{
//No-arg JAXB constructor
setProtocol(Protocol.NXDN);
}
/**
* Constructs an instance
* @param homeRepeater for the radio
* @param radio value, without the home repeater
*/
public NXDNTypeDRadio(int homeRepeater, int radio)
{
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, radio));
}
@Override
public AliasIDType getType()
{
return AliasIDType.NXDN_TYPE_D_RADIO_ID;
}
@JsonIgnore
@Override
public int getHomeRepeater()
{
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue());
}
@Override
public void setHomeRepeater(int homeRepeater)
{
setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier()));
}
@JsonIgnore
@Override
public int getIdentifier()
{
return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue());
}
@Override
public void setIdentifier(int identifier)
{
setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Type-D Radio ID:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true));
sb.append(" Protocol:").append(getProtocol());
if(!isValid())
{
sb.append(" **NOT VALID**");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,124 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.radio;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.INXDNTypeDRange;
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN Type-D radio range for a single home repeater. The min and max values are the over-the-air 16-bit identifiers
* that combine the home repeater and radio.
*/
public class NXDNTypeDRadioRange extends RadioRange implements INXDNTypeDRange
{
public NXDNTypeDRadioRange()
{
//No-arg JAXB constructor
setProtocol(Protocol.NXDN);
}
/**
* Constructs an instance
* @param homeRepeater for the range
* @param minRadio value, without the home repeater
* @param maxRadio value, without the home repeater
*/
public NXDNTypeDRadioRange(int homeRepeater, int minRadio, int maxRadio)
{
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minRadio),
NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxRadio));
}
@Override
public AliasIDType getType()
{
return AliasIDType.NXDN_TYPE_D_RADIO_ID_RANGE;
}
@JsonIgnore
@Override
public int getHomeRepeater()
{
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinRadio());
}
@Override
public void setHomeRepeater(int homeRepeater)
{
int min = getMinIdentifier();
int max = getMaxIdentifier();
setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min));
setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max));
}
@JsonIgnore
@Override
public int getMinIdentifier()
{
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinRadio());
}
@Override
public void setMinIdentifier(int identifier)
{
setMinRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
}
@JsonIgnore
@Override
public int getMaxIdentifier()
{
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxRadio());
}
@Override
public void setMaxIdentifier(int identifier)
{
setMaxRadio(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
}
/**
* Valid when the base range is valid and both ends of the range use the same home repeater.
*/
@Override
public boolean isValid()
{
return super.isValid() && getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxRadio());
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Type-D Radio ID Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true));
sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true));
sb.append(" Protocol:").append(getProtocol());
if(!isValid())
{
sb.append(" **NOT VALID**");
}
return sb.toString();
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.radio;
@@ -38,10 +35,11 @@ public enum RadioFormat
"<html>APCO25 valid range is 0 to 16,777,215"),
DMR("********", 0, 0xFFFFFF, "0 to 16,777,215",
"<html>DMR unit id valid range is 0 to 16,777,215"),
NXDN("*****", 1, 0xFFFF, "1 to 65,535", "NXDN valid range is 1 to 65,535"),
PASSPORT("********", 0, 0x7FFFFF, "0 to 8,388,607",
"<html>PASSPORT valid range is 0 to 8,388,607"),
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
"Unknown protocol valid value range is 1-16,777,215");
"<html>Unknown protocol valid value range is 1-16,777,215");
private String mMask;
private int mMinimumValue;
@@ -116,6 +114,8 @@ public enum RadioFormat
return APCO25;
case DMR:
return DMR;
case NXDN:
return NXDN;
case PASSPORT:
return PASSPORT;
default:

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.radio;
@@ -26,14 +23,14 @@ import io.github.dsheirer.preference.identifier.IntegerFormat;
import io.github.dsheirer.preference.identifier.talkgroup.APCO25TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.AbstractIntegerFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.DMRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.ParseException;
import java.util.EnumMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Protocol-specific formatting of radio id values and parsing of formatted radio id values.
@@ -47,6 +44,7 @@ public class RadioFormatter
{
mFormatterMap.put(Protocol.APCO25, new APCO25TalkgroupFormatter());
mFormatterMap.put(Protocol.DMR, new DMRTalkgroupFormatter());
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}

View File

@@ -0,0 +1,96 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.talkgroup;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.INXDNTypeD;
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN Type-D talkgroup. The value is the over-the-air 16-bit identifier that combines the home repeater and talkgroup
* so that it matches the value of an NXDN Type-D talkgroup identifier.
*/
public class NXDNTypeDTalkgroup extends Talkgroup implements INXDNTypeD
{
public NXDNTypeDTalkgroup()
{
//No-arg JAXB constructor
setProtocol(Protocol.NXDN);
}
/**
* Constructs an instance
* @param homeRepeater for the talkgroup
* @param talkgroup value, without the home repeater
*/
public NXDNTypeDTalkgroup(int homeRepeater, int talkgroup)
{
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, talkgroup));
}
@Override
public AliasIDType getType()
{
return AliasIDType.NXDN_TYPE_D_TALKGROUP;
}
@JsonIgnore
@Override
public int getHomeRepeater()
{
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getValue());
}
@Override
public void setHomeRepeater(int homeRepeater)
{
setValue(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, getIdentifier()));
}
@JsonIgnore
@Override
public int getIdentifier()
{
return NXDNTalkgroupFormatter.getTypeDIdentifier(getValue());
}
@Override
public void setIdentifier(int identifier)
{
setValue(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Type-D Talkgroup:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getIdentifier(), true));
sb.append(" Protocol:").append(getProtocol());
if(!isValid())
{
sb.append(" **NOT VALID**");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,125 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.talkgroup;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.github.dsheirer.alias.id.AliasIDType;
import io.github.dsheirer.alias.id.INXDNTypeDRange;
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
/**
* NXDN Type-D talkgroup range for a single home repeater. The min and max values are the over-the-air 16-bit
* identifiers that combine the home repeater and talkgroup.
*/
public class NXDNTypeDTalkgroupRange extends TalkgroupRange implements INXDNTypeDRange
{
public NXDNTypeDTalkgroupRange()
{
//No-arg JAXB constructor
setProtocol(Protocol.NXDN);
}
/**
* Constructs an instance
* @param homeRepeater for the range
* @param minTalkgroup value, without the home repeater
* @param maxTalkgroup value, without the home repeater
*/
public NXDNTypeDTalkgroupRange(int homeRepeater, int minTalkgroup, int maxTalkgroup)
{
super(Protocol.NXDN, NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, minTalkgroup),
NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, maxTalkgroup));
}
@Override
public AliasIDType getType()
{
return AliasIDType.NXDN_TYPE_D_TALKGROUP_RANGE;
}
@JsonIgnore
@Override
public int getHomeRepeater()
{
return NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMinTalkgroup());
}
@Override
public void setHomeRepeater(int homeRepeater)
{
int min = getMinIdentifier();
int max = getMaxIdentifier();
setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, min));
setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(homeRepeater, max));
}
@JsonIgnore
@Override
public int getMinIdentifier()
{
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMinTalkgroup());
}
@Override
public void setMinIdentifier(int identifier)
{
setMinTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
}
@JsonIgnore
@Override
public int getMaxIdentifier()
{
return NXDNTalkgroupFormatter.getTypeDIdentifier(getMaxTalkgroup());
}
@Override
public void setMaxIdentifier(int identifier)
{
setMaxTalkgroup(NXDNTalkgroupFormatter.toTypeDValue(getHomeRepeater(), identifier));
}
/**
* Valid when the base range is valid and both ends of the range use the same home repeater.
*/
@Override
public boolean isValid()
{
return super.isValid() &&
getHomeRepeater() == NXDNTalkgroupFormatter.getTypeDHomeRepeater(getMaxTalkgroup());
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("Type-D Talkgroup Range:").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMinIdentifier(), true));
sb.append(" to ").append(NXDNTalkgroupFormatter.toTypeD(getHomeRepeater(), getMaxIdentifier(), true));
sb.append(" Protocol:").append(getProtocol());
if(!isValid())
{
sb.append(" **NOT VALID**");
}
return sb.toString();
}
}

View File

@@ -0,0 +1,40 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.talkgroup;
import io.github.dsheirer.protocol.Protocol;
/**
* Streaming talkgroup alias. For streamed audio metadata, this value is used to replace the decoded talkgroup or TO
* value with an alias value when there are talkgroup collisions resulting from streaming multiple channels to the
* same stream. This is common in P25 conventional repeater setups where each radio channel is using talkgroup 1.
*/
public class StreamAsTalkgroup extends Talkgroup
{
public StreamAsTalkgroup(int talkgroup)
{
super(Protocol.UNKNOWN, talkgroup);
}
public StreamAsTalkgroup()
{
//No arg JAXB constructor
}
}

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.alias.id.talkgroup;
@@ -50,6 +47,8 @@ public enum TalkgroupFormat
"<html>MPT-1327 valid ranges are 0-127(prefix)<br>and 1-8192(ident) (ie. 000-0001 to 127-8192)"),
NBFM("*****", 1, 0xFFFF, "1 to 65,535",
"NBFM valid value range is 1-65,535"),
NXDN("*****", 0, 0xFFFF, "0 to 65,535",
"NXDN talkgroup valid range is 0 to 65,535"),
PASSPORT("*****", 1, 0xFFFF, "1 to 65,535",
"Passport valid value range is 1-65,535"),
UNKNOWN("********", 1, 0xFFFFFF, "1 to 16,777,215",
@@ -139,6 +138,8 @@ public enum TalkgroupFormat
return MPT1327;
case NBFM:
return NBFM;
case NXDN:
return NXDN;
case PASSPORT:
return PASSPORT;
default:

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -28,6 +28,7 @@ import io.github.dsheirer.preference.identifier.talkgroup.FleetsyncTalkgroupForm
import io.github.dsheirer.preference.identifier.talkgroup.LTRTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MDC1200TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.MPT1327TalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.NXDNTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.PassportTalkgroupFormatter;
import io.github.dsheirer.preference.identifier.talkgroup.UnknownTalkgroupFormatter;
import io.github.dsheirer.protocol.Protocol;
@@ -59,6 +60,7 @@ public class TalkgroupFormatter
mFormatterMap.put(Protocol.LTR_NET, ltr);
mFormatterMap.put(Protocol.MDC1200, new MDC1200TalkgroupFormatter());
mFormatterMap.put(Protocol.MPT1327, new MPT1327TalkgroupFormatter());
mFormatterMap.put(Protocol.NXDN, new NXDNTalkgroupFormatter());
mFormatterMap.put(Protocol.PASSPORT, new PassportTalkgroupFormatter());
mFormatterMap.put(Protocol.UNKNOWN, new UnknownTalkgroupFormatter());
}

View File

@@ -0,0 +1,17 @@
package io.github.dsheirer.audio;
/**
* Used for defining an array list of filter methods to be implemented in AudioModule
* The filters will be applied in the order given in the list.
* Each audio filter mush implement the filter method.
*/
public abstract class AbstractAudioFilter
{
/**
* To be overridden by each audio filter
* @param audio a buffer of 8 KHz audio samples
* @return float[] filtered buffer of 8 KHz audio samples
*/
public abstract float[] filter(float[] audio);
}

View File

@@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory;
public abstract class AbstractAudioModule extends Module implements IAudioSegmentProvider, IdentifierUpdateListener
{
private final static Logger mLog = LoggerFactory.getLogger(AbstractAudioModule.class);
public static final long DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH = 60 * 8000; // 1 minute @ 8kHz
public static final long DEFAULT_MAX_SEGMENT_LENGTH_MILLIS = 60_000; // 1 minute
public static final int DEFAULT_TIMESLOT = 0;
private final int mMaxSegmentAudioSampleLength;
private Listener<AudioSegment> mAudioSegmentListener;
@@ -51,12 +51,12 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
* Constructs an abstract audio module
*
* @param aliasList for aliasing identifiers
* @param maxSegmentAudioSampleLength in milliseconds
* @param maxSegmentLengthMillis maximum audio segment length in milliseconds
*/
public AbstractAudioModule(AliasList aliasList, int timeslot, long maxSegmentAudioSampleLength)
public AbstractAudioModule(AliasList aliasList, int timeslot, long maxSegmentLengthMillis)
{
mAliasList = aliasList;
mMaxSegmentAudioSampleLength = (int)(maxSegmentAudioSampleLength * 8); //Convert milliseconds to samples
mMaxSegmentAudioSampleLength = (int)(maxSegmentLengthMillis * 8); //Convert milliseconds to samples
mTimeslot = timeslot;
mIdentifierCollection = new MutableIdentifierCollection(getTimeslot());
mIdentifierUpdateNotificationBroadcaster.addListener(mIdentifierCollection);
@@ -67,7 +67,7 @@ public abstract class AbstractAudioModule extends Module implements IAudioSegmen
*/
public AbstractAudioModule(AliasList aliasList)
{
this(aliasList, DEFAULT_TIMESLOT, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
this(aliasList, DEFAULT_TIMESLOT, DEFAULT_MAX_SEGMENT_LENGTH_MILLIS);
}
/**

View File

@@ -0,0 +1,44 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.dsp.gain.AudioGainAndDcFilter;
/**
* This is a wrapper for the AudioGainAndDcFilter class so it can be used with the AudioModule filter list.
* Usually instantiated in the DecoderFactory
*/
public class AudioGainFilter extends AbstractAudioFilter
{
AudioGainAndDcFilter mAudioGain;
public AudioGainFilter(float minGain, float maxGain, float objectiveAmplitude)
{
mAudioGain = new AudioGainAndDcFilter(minGain, maxGain, objectiveAmplitude);
mAudioGain.setDecayRate(2); // to remove pumping effect
mAudioGain.reset();
}
@Override
public float[] filter(float[] audio)
{
return mAudioGain.process(audio);
}
}

View File

@@ -32,6 +32,8 @@ import io.github.dsheirer.sample.real.IRealBufferListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
/**
* Provides packaging of demodulated audio sample buffers into audio segments for broadcast to registered listeners.
* Includes audio packet metadata in constructed audio segments.
@@ -42,36 +44,7 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
Listener<float[]>
{
private static final Logger mLog = LoggerFactory.getLogger(AudioModule.class);
private static float[] sHighPassFilterCoefficients;
private final boolean mAudioFilterEnable;
static
{
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
.sampleRate(8000)
.stopBandCutoff(200)
.stopBandAmplitude(0.0)
.stopBandRipple(0.025)
.passBandStart(300)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.build();
try
{
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
if(designer.isValid())
{
sHighPassFilterCoefficients = designer.getImpulseResponse();
}
}
catch(FilterDesignException fde)
{
mLog.error("Filter design error", fde);
}
}
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
private List<AbstractAudioFilter>mAudioFilters;
private final SquelchStateListener mSquelchStateListener = new SquelchStateListener();
private SquelchState mSquelchState = SquelchState.SQUELCH;
@@ -81,23 +54,23 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
* @param aliasList for aliasing identifiers
* @param timeslot for this audio module
* @param maxAudioSegmentLength in milliseconds
* @param audioFilterEnable to enable or disable high-pass audio filter
* @param audioFilters a list of audio filters to apply to the incoming audio samples
*/
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, boolean audioFilterEnable)
public AudioModule(AliasList aliasList, int timeslot, long maxAudioSegmentLength, List<AbstractAudioFilter> audioFilters)
{
super(aliasList, timeslot, maxAudioSegmentLength);
mAudioFilterEnable = audioFilterEnable;
mAudioFilters = audioFilters;
}
/**
* Creates an Audio Module.
* @param aliasList for aliasing identifiers
* @param audioFilterEnable to enable or disable high-pass audio filter
* @param audioFilters a list of audio filters to apply to the incoming audio samples
*/
public AudioModule(AliasList aliasList, boolean audioFilterEnable)
public AudioModule(AliasList aliasList, List<AbstractAudioFilter> audioFilters)
{
super(aliasList);
mAudioFilterEnable = audioFilterEnable;
mAudioFilters = audioFilters;
}
@Override
@@ -128,9 +101,13 @@ public class AudioModule extends AbstractAudioModule implements ISquelchStateLis
{
if(mSquelchState == SquelchState.UNSQUELCH)
{
if(mAudioFilterEnable)
// apply audio filters if any (i.e. deemphasis, high pass, etc.)
if(!mAudioFilters.isEmpty())
{
audioBuffer = mHighPassFilter.filter(audioBuffer);
for(AbstractAudioFilter filter : mAudioFilters)
{
audioBuffer = filter.filter(audioBuffer);
}
}
addAudio(audioBuffer);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -61,7 +61,7 @@ import org.slf4j.LoggerFactory;
*
* Producers will add audio buffers and update identifiers throughout the life-cycle of an audio segment. The producer
* will signal the completion of an audio segment by setting the complete property to true. This allows consumers the
* option to process the audio buffers throughout the life-cycle of the segment, or to process all of the buffers once
* option to process the audio buffers throughout the life-cycle of the segment, or to process all the buffers once
* the segment is complete.
*/
public class AudioSegment implements Listener<IdentifierUpdateNotification>

View File

@@ -0,0 +1,50 @@
package io.github.dsheirer.audio;
import io.github.dsheirer.dsp.filter.FilterFactory;
import io.github.dsheirer.dsp.filter.design.FilterDesignException;
import io.github.dsheirer.dsp.filter.fir.FIRFilterSpecification;
import io.github.dsheirer.dsp.filter.fir.real.IRealFilter;
import io.github.dsheirer.dsp.filter.fir.remez.RemezFIRFilterDesigner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HighPassAudioFilter extends AbstractAudioFilter
{
private static final Logger mLog = LoggerFactory.getLogger(HighPassAudioFilter.class);
private static float[] sHighPassFilterCoefficients;
static
{
FIRFilterSpecification specification = FIRFilterSpecification.highPassBuilder()
.sampleRate(8000)
.stopBandCutoff(200)
.stopBandAmplitude(0.0)
.stopBandRipple(0.025)
.passBandStart(300)
.passBandAmplitude(1.0)
.passBandRipple(0.01)
.build();
try
{
RemezFIRFilterDesigner designer = new RemezFIRFilterDesigner(specification);
if(designer.isValid())
{
sHighPassFilterCoefficients = designer.getImpulseResponse();
}
}
catch(FilterDesignException fde)
{
mLog.error("Filter design error", fde);
}
}
private final IRealFilter mHighPassFilter = FilterFactory.getRealFilter(sHighPassFilterCoefficients);
@Override
public float[] filter(float[] audio)
{
audio = mHighPassFilter.filter(audio);
return audio;
}
}

View File

@@ -1,9 +1,27 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio;
import io.github.dsheirer.audio.playback.AudioOutput;
import io.github.dsheirer.audio.playback.AudioChannel;
import io.github.dsheirer.audio.playback.AudioPlaybackDeviceDescriptor;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import java.util.List;
/**
@@ -12,13 +30,13 @@ import java.util.List;
public interface IAudioController
{
/* Current Mixer and MixerChannel configuration */
public void setMixerChannelConfiguration( MixerChannelConfiguration entry ) throws AudioException;
public MixerChannelConfiguration getMixerChannelConfiguration() throws AudioException;
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor device ) throws AudioException;
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice() throws AudioException;
/* Audio Output(s) */
public List<AudioOutput> getAudioOutputs();
/* Audio Channel(s) */
public List<AudioChannel> getAudioChannels();
/* Controller Audio Event Listener */
public void addControllerListener( Listener<AudioEvent> listener );
public void removeControllerListener( Listener<AudioEvent> listener );
public void addAudioEventListener(Listener<AudioEvent> listener );
public void removeAudioEventListener(Listener<AudioEvent> listener );
}

View File

@@ -36,6 +36,7 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
protected ObjectProperty<BroadcastState> mBroadcastState = new SimpleObjectProperty<>(BroadcastState.READY);
protected ObjectProperty<BroadcastState> mLastBadBroadcastState = new SimpleObjectProperty<>();
protected int mStreamedAudioCount = 0;
protected int mDuplicateRejectedAudioCount = 0;
protected int mErrorAudioCount = 0;
protected int mAgedOffAudioCount = 0;
@@ -163,6 +164,22 @@ public abstract class AbstractAudioBroadcaster<T extends BroadcastConfiguration>
mStreamedAudioCount++;
}
/**
* Number of audio recordings rejected by the remote service as duplicates.
*/
public int getDuplicateRejectedAudioCount()
{
return mDuplicateRejectedAudioCount;
}
/**
* Increments the duplicate-rejected audio count by one.
*/
public void incrementDuplicateRejectedAudioCount()
{
mDuplicateRejectedAudioCount++;
}
/**
* Number of audio recordings that were removed for exceeding age limit
*/

View File

@@ -82,6 +82,7 @@ public class BroadcastEvent
BROADCASTER_QUEUE_CHANGE,
BROADCASTER_STATE_CHANGE,
BROADCASTER_STREAMED_COUNT_CHANGE,
BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE,
BROADCASTER_AGED_OFF_COUNT_CHANGE,
BROADCASTER_ERROR_COUNT_CHANGE,
BROADCASTER_DELETE,

View File

@@ -62,11 +62,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
public static final int COLUMN_BROADCASTER_STATUS = 2;
public static final int COLUMN_BROADCASTER_QUEUE_SIZE = 3;
public static final int COLUMN_BROADCASTER_STREAMED_COUNT = 4;
public static final int COLUMN_BROADCASTER_AGED_OFF_COUNT = 5;
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 6;
public static final int COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT = 5;
public static final int COLUMN_BROADCASTER_AGED_OFF_COUNT = 6;
public static final int COLUMN_BROADCASTER_ERROR_COUNT = 7;
public static final String[] COLUMN_NAMES = new String[]
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Aged Off", "Upload Error"};
{"Stream Type", "Name", "Status", "Queued", "Streamed/Uploaded", "Duplicate Rejected", "Aged Off", "Upload Error"};
private ObservableList<ConfiguredBroadcast> mConfiguredBroadcasts =
FXCollections.observableArrayList(ConfiguredBroadcast.extractor());
@@ -361,7 +362,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
}
broadcast(new BroadcastEvent(audioBroadcaster, BroadcastEvent.Event.BROADCASTER_ADD));
ThreadPool.CACHED.submit(audioBroadcaster::start);
ThreadPool.CACHED.submit(() -> audioBroadcaster.start());
}
}
}
@@ -531,6 +532,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
fireTableCellUpdated(row, COLUMN_BROADCASTER_STREAMED_COUNT);
}
break;
case BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE:
if(row >= 0)
{
fireTableCellUpdated(row, COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT);
}
break;
case BROADCASTER_AGED_OFF_COUNT_CHANGE:
if(row >= 0)
{
@@ -607,6 +614,12 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
return configuredBroadcast.getAudioBroadcaster().getStreamedAudioCount();
}
break;
case COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT:
if(configuredBroadcast.hasAudioBroadcaster())
{
return configuredBroadcast.getAudioBroadcaster().getDuplicateRejectedAudioCount();
}
break;
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
if(configuredBroadcast.hasAudioBroadcaster())
{
@@ -643,6 +656,7 @@ public class BroadcastModel extends AbstractTableModel implements Listener<Audio
case COLUMN_BROADCASTER_AGED_OFF_COUNT:
case COLUMN_BROADCASTER_QUEUE_SIZE:
case COLUMN_BROADCASTER_STREAMED_COUNT:
case COLUMN_BROADCASTER_DUPLICATE_REJECTED_COUNT:
case COLUMN_BROADCASTER_ERROR_COUNT:
return Integer.class;
case COLUMN_BROADCAST_SERVER_TYPE:

View File

@@ -151,7 +151,10 @@ public class BroadcastStatusPanel extends JPanel
else if(state == BroadcastState.DISABLED)
{
setBackground(table.getBackground());
setForeground(Color.LIGHT_GRAY);
//Use the LAF's disabled-foreground so the cell reads correctly on any
//theme - LIGHT_GRAY is invisible on a light background.
Color disabled = javax.swing.UIManager.getColor("Label.disabledForeground");
setForeground(disabled != null ? disabled : table.getForeground());
}
else if(state == BroadcastState.INVALID_SETTINGS ||
state == BroadcastState.NETWORK_UNAVAILABLE)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,6 +20,8 @@
package io.github.dsheirer.audio.broadcast.broadcastify;
import com.google.common.net.HttpHeaders;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
@@ -32,6 +34,8 @@ import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
import io.github.dsheirer.identifier.configuration.AliasListConfigurationIdentifier;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
@@ -45,6 +49,7 @@ import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
@@ -76,6 +81,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
.build();
private long mLastConnectionAttempt;
private long mConnectionAttemptInterval = 5000; //Every 5 seconds
final private AliasModel mAliasModel;
/**
* Constructs an instance of the broadcaster
@@ -86,6 +92,7 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
MP3Setting mp3Setting, AliasModel aliasModel)
{
super(config);
mAliasModel = aliasModel;
}
/**
@@ -247,6 +254,8 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
String radioId = getFrom(audioRecording);
float frequency = getFrequency(audioRecording);
String radioIdAlias = getFromAlias(audioRecording);
BroadcastifyCallBuilder bodyBuilder = new BroadcastifyCallBuilder();
bodyBuilder.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
.addPart(FormField.SYSTEM_ID, getBroadcastConfiguration().getSystemID())
@@ -257,6 +266,11 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
.addPart(FormField.FREQUENCY, frequency)
.addPart(FormField.ENCODING, ENCODING_TYPE_MP3);
if(radioIdAlias != null && !radioIdAlias.isEmpty())
{
bodyBuilder.addPart(FormField.RADIO_ID_ALIAS, radioIdAlias);
}
try
{
HttpRequest request = HttpRequest.newBuilder()
@@ -356,6 +370,9 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
else if(urlResponse.startsWith("1 SKIPPED"))
{
//Broadcastify is telling us to skip audio upload - someone already uploaded it
incrementDuplicateRejectedAudioCount();
broadcast(new BroadcastEvent(BroadcastifyCallBroadcaster.this,
BroadcastEvent.Event.BROADCASTER_DUPLICATE_REJECTED_COUNT_CHANGE));
audioRecording.removePendingReplay();
}
else
@@ -440,13 +457,58 @@ public class BroadcastifyCallBroadcaster extends AbstractAudioBroadcaster<Broadc
return "0";
}
/**
* Resolves the over-the-air talker alias for the FROM (source radio) identifier, acquired in real-time from RF
* (e.g. P25 Motorola Talker Alias or DMR Talker Alias). Returns null if no OTA alias was received for this call.
* This is sent as the srcId_alias field in Broadcastify uploads.
*/
private static String getFromAlias(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof TalkerAliasIdentifier talkerAliasIdentifier)
{
if(talkerAliasIdentifier.isValid())
{
return talkerAliasIdentifier.getValue();
}
}
}
return null;
}
/**
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*/
private static String getTo(AudioRecording audioRecording)
private String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
//Alias the TO value when the user specifies a 'Stream As Talkgroup'
if(identifier != null)
{
AliasListConfigurationIdentifier config = audioRecording.getIdentifierCollection().getAliasListConfiguration();
if(config != null)
{
AliasList aliasList = mAliasModel.getAliasList(config.getValue());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
for(Alias a: aliases)
{
if(a.getStreamTalkgroupAlias() != null)
{
return String.valueOf(a.getStreamTalkgroupAlias().getValue());
}
}
}
}
}
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return format(patchGroupIdentifier);

View File

@@ -30,6 +30,7 @@ public enum FormField
TIMESTAMP("ts"),
TALKGROUP_ID("tg"),
RADIO_ID("src"),
RADIO_ID_ALIAS("srcId_alias"),
FREQUENCY("freq"),
ENCODING("enc"),
TEST("test");

View File

@@ -52,8 +52,10 @@ import org.slf4j.LoggerFactory;
public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
private static final Logger mLog = LoggerFactory.getLogger(IcecastHTTPAudioBroadcaster.class);
private static final long CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS = 5000; // 5 seconds
private static final long RECONNECT_INTERVAL_MILLISECONDS = 30000; //30 seconds
private static final String HTTP_1_0_OK_HEX_DUMP = "48 54 54 50 2F 31 2E 30 20 32 30 30 20 4F 4B";
private static final String HTTP_1_1_CONTINUE_HEX_DUMP = "48 54 54 50 2F 31 2E 31 20 31 30 30";
private NioSocketConnector mSocketConnector;
private IoSession mStreamingSession = null;
@@ -61,6 +63,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
private long mLastConnectionAttempt = 0;
private AtomicBoolean mConnecting = new AtomicBoolean();
private AtomicBoolean mConnectionFailureLogged = new AtomicBoolean(false);
/**
* Creates an Icecast 2.4.x compatible broadcaster using HTTP 1.1 protocol. This broadcaster is
@@ -141,7 +144,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
if(mSocketConnector == null)
{
mSocketConnector = new NioSocketConnector();
mSocketConnector.setConnectTimeoutCheckInterval(10000);
mSocketConnector.setConnectTimeoutMillis(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
mSocketConnector.setConnectTimeoutCheckInterval(1000);
// LoggingFilter loggingFilter = new LoggingFilter(IcecastHTTPAudioBroadcaster.class);
// loggingFilter.setMessageReceivedLogLevel(LogLevel.DEBUG);
@@ -160,37 +164,58 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
setBroadcastState(BroadcastState.CONNECTING);
try
{
try {
ConnectFuture future = mSocketConnector
.connect(new InetSocketAddress(getBroadcastConfiguration().getHost(),
getBroadcastConfiguration().getPort()));
future.awaitUninterruptibly();
mStreamingSession = future.getSession();
}
catch(RuntimeIoException rie)
{
boolean connected;
try {
connected = future.await(CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
if (mConnectionFailureLogged.compareAndSet(false, true)) {
mLog.debug("HTTP connect attempt interrupted");
}
setBroadcastState(BroadcastState.DISCONNECTED);
mLastConnectionAttempt = System.currentTimeMillis();
future.cancel();
disconnect();
return;
}
if (connected && future.isConnected()) {
mStreamingSession = future.getSession();
mConnecting.set(false);
mConnectionFailureLogged.set(false);
return;
} else {
if (mConnectionFailureLogged.compareAndSet(false, true)) {
mLog.debug("HTTP connect attempt timed out ({} ms) or not connected",
CONNECTION_ATTEMPT_TIMEOUT_MILLISECONDS);
}
setBroadcastState(BroadcastState.DISCONNECTED);
mLastConnectionAttempt = System.currentTimeMillis();
future.cancel();
disconnect();
}
} catch (RuntimeIoException rie) {
Throwable throwableCause = rie.getCause();
if(throwableCause instanceof ConnectException)
{
setBroadcastState(BroadcastState.NO_SERVER);
}
else if(throwableCause != null)
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLog.debug("Failed to connect", rie);
}
else
{
setBroadcastState(BroadcastState.DISCONNECTED);
mLog.debug("Failed to connect - no exception is available");
}
setBroadcastState(BroadcastState.DISCONNECTED);
if (mConnectionFailureLogged.compareAndSet(false, true)) {
if (throwableCause != null) {
mLog.debug("Failed to connect", rie);
} else {
mLog.debug("Failed to connect - no exception is available");
}
}
mLastConnectionAttempt = System.currentTimeMillis();
disconnect();
} finally {
if (!connected()) {
mConnecting.set(false);
}
}
mConnecting.set(false);
}
};
@@ -210,6 +235,11 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
mStreamingSession.closeNow();
}
mLastConnectionAttempt = System.currentTimeMillis();
if(!getBroadcastState().isErrorState()) {
setBroadcastState(BroadcastState.DISCONNECTED);
}
}
/**
@@ -233,6 +263,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
mHTTPHeaders = new HashMap<>();
mHTTPHeaders.put(IcecastHeader.ACCEPT.getValue(), "*/*");
mHTTPHeaders.put(IcecastHeader.EXPECT.getValue(), "100-continue");
mHTTPHeaders.put(IcecastHeader.CONTENT_TYPE.getValue(), getConfiguration().getBroadcastFormat().getValue());
mHTTPHeaders.put(IcecastHeader.USER_AGENT.getValue(), SystemProperties.getInstance().getApplicationName());
mHTTPHeaders.put(IcecastHeader.AUTHORIZATION.getValue(), getConfiguration().getBase64EncodedCredentials());
@@ -288,6 +319,7 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
@Override
public void sessionClosed(IoSession session) throws Exception
{
mLastConnectionAttempt = System.currentTimeMillis();
//If there is already an error state, don't override it. Otherwise, set state to disconnected
if(!getBroadcastState().isErrorState())
{
@@ -320,9 +352,10 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
{
String hexDump = ((ProtocolDecoderException)throwable).getHexdump();
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP))
if(hexDump.startsWith(HTTP_1_0_OK_HEX_DUMP) || hexDump.startsWith(HTTP_1_1_CONTINUE_HEX_DUMP))
{
setBroadcastState(BroadcastState.CONNECTED);
mConnectionFailureLogged.set(false);
}
else
{
@@ -401,10 +434,15 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
switch(response.getStatus())
{
case INFORMATIONAL_CONTINUE:
//Icecast 2.5+ signals that we can start sending audio with 100-continue
setBroadcastState(BroadcastState.CONNECTED);
mConnecting.set(false);
mConnectionFailureLogged.set(false);
break;
case SUCCESS_OK:
setBroadcastState(BroadcastState.CONNECTED);
mConnecting.set(false);
mConnectionFailureLogged.set(false);
break;
case CLIENT_ERROR_UNAUTHORIZED:
setBroadcastState(BroadcastState.INVALID_CREDENTIALS);
@@ -468,7 +506,8 @@ public class IcecastHTTPAudioBroadcaster extends IcecastAudioBroadcaster
mMessage = new String(bytes);
Pattern pattern = Pattern.compile("HTTP/1.0 (\\d{3})");
//Icecast 2.4+ responds with HTTP/1.0 and Icecast 2.5+ responds with HTTP/1.1
Pattern pattern = Pattern.compile("HTTP/1\\.[01] (\\d{3})");
Matcher m = pattern.matcher(mMessage);
if(m.find())

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.broadcast.icecast;
@@ -25,21 +22,16 @@ import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.icecast.IcecastConfiguration;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.List;
public class IcecastMetadata
{
private final static Logger mLog = LoggerFactory.getLogger(IcecastMetadata.class);
@@ -73,13 +65,23 @@ public class IcecastMetadata
if(to != null)
{
sb.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
if(aliases != null && !aliases.isEmpty())
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
}
else
{
sb.append("TO:").append(to);
if(!aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
}
else

View File

@@ -34,7 +34,8 @@
API_KEY("api_key"),
SOURCE_LIST("source_list"),
FREQ_LIST("freq_list"),
PATCHES("patches");
PATCH_LIST("patch_list"),
TALKER_ALIAS("talker_alias");
private String mHeader;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -19,55 +19,45 @@
package io.github.dsheirer.audio.broadcast.openmhz;
import com.google.common.net.HttpHeaders;
import com.google.common.base.Joiner;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.MutableIdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.nio.file.Files;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ConnectException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import com.google.common.net.HttpHeaders;
import io.github.dsheirer.alias.Alias;
import io.github.dsheirer.alias.AliasList;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.broadcast.AbstractAudioBroadcaster;
import io.github.dsheirer.audio.broadcast.AudioRecording;
import io.github.dsheirer.audio.broadcast.BroadcastEvent;
import io.github.dsheirer.audio.broadcast.BroadcastState;
import io.github.dsheirer.audio.convert.InputAudioFormat;
import io.github.dsheirer.audio.convert.MP3Setting;
import io.github.dsheirer.gui.playlist.radioreference.RadioReferenceDecoder;
import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.util.ThreadPool;
import java.io.IOException;
import java.net.ConnectException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -249,6 +239,7 @@
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
String talkgroup = getTo(audioRecording);
String radioId = getFrom(audioRecording);
String talkerAlias = getTalkerAlias(audioRecording);
Long frequency = getFrequency(audioRecording);
String patches = getPatches(audioRecording);
String talkgroupLabel = getTalkgroupLabel(audioRecording);
@@ -282,8 +273,8 @@
.addPart(FormField.TALKGROUP_NUM, talkgroup)
.addPart(FormField.EMERGENCY, 0)
.addPart(FormField.API_KEY, getBroadcastConfiguration().getApiKey())
// For future use if OpenMHz supports patch information
//.addPart(FormField.PATCHES, patches)
.addPart(FormField.PATCH_LIST, patches)
.addPart(FormField.TALKER_ALIAS, talkerAlias)
.addPart(FormField.SOURCE_LIST, "[{ \"pos\": 0.00, \"src\": " + radioId + "}]");
HttpRequest fileRequest = HttpRequest.newBuilder()
@@ -426,22 +417,40 @@
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*
*/
private static String getTo(AudioRecording audioRecording)
private String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
if(identifier != null)
{
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
}
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
}
}
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
}
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
}
}
return "0";
@@ -547,6 +556,28 @@
return "[]";
}
/**
* Creates a formatted string with talkerAlias identifier
*/
private static String getTalkerAlias(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof TalkerAliasIdentifier)
{
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
if(talkerID.isValid())
{
return talkerID.getValue();
}
}
}
return "";
}
/**
* Tests both the connection and configuration against the OpenMHz Call API service
* @param configuration containing API key and system id

View File

@@ -17,13 +17,14 @@
* ****************************************************************************
*/
package io.github.dsheirer.gui.playlist.streaming;
package io.github.dsheirer.audio.broadcast.openmhz;
import io.github.dsheirer.audio.broadcast.BroadcastServerType;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzBroadcaster;
import io.github.dsheirer.audio.broadcast.openmhz.OpenMHzConfiguration;
import io.github.dsheirer.gui.control.IntegerTextField;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.gui.playlist.streaming.AbstractBroadcastEditor;
import io.github.dsheirer.playlist.PlaylistManager;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;

View File

@@ -36,6 +36,7 @@ public enum FormField
SOURCES("sources"),
SYSTEM("system"),
SYSTEM_LABEL("systemLabel"),
TALKER_ALIAS("talkerAlias"),
TALKGROUP_ID("talkgroup"),
TALKGROUP_GROUP("talkgroupGroup"),
TALKGROUP_LABEL("talkgroupLabel"),

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 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
@@ -34,6 +34,7 @@ import io.github.dsheirer.identifier.Form;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.identifier.alias.TalkerAliasIdentifier;
import io.github.dsheirer.identifier.configuration.ConfigurationLongIdentifier;
import io.github.dsheirer.identifier.patch.PatchGroup;
import io.github.dsheirer.identifier.patch.PatchGroupIdentifier;
@@ -48,6 +49,7 @@ import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.CompletionException;
import java.util.concurrent.LinkedTransferQueue;
@@ -227,6 +229,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
long timestampSeconds = (int)(audioRecording.getStartTime() / 1E3);
String talkgroup = getTo(audioRecording);
String radioId = getFrom(audioRecording);
String talkerAlias = getTalkerAlias(audioRecording);
Long frequency = getFrequency(audioRecording);
String patches = getPatches(audioRecording);
String talkgroupLabel = getTalkgroupLabel(audioRecording);
@@ -261,6 +264,7 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
.addPart(FormField.TALKGROUP_ID, talkgroup)
.addPart(FormField.SOURCE, radioId)
.addPart(FormField.FREQUENCY, frequency)
.addPart(FormField.TALKER_ALIAS, talkerAlias)
.addPart(FormField.TALKGROUP_LABEL, talkgroupLabel)
.addPart(FormField.TALKGROUP_GROUP, talkgroupGroup)
.addPart(FormField.SYSTEM_LABEL, systemLabel)
@@ -408,26 +412,62 @@ public class RdioScannerBroadcaster extends AbstractAudioBroadcaster<RdioScanner
return "0";
}
private static String getTalkerAlias(AudioRecording audioRecording)
{
for(Identifier identifier: audioRecording.getIdentifierCollection().getIdentifiers(Role.FROM))
{
if(identifier instanceof TalkerAliasIdentifier)
{
TalkerAliasIdentifier talkerID = (TalkerAliasIdentifier)identifier;
if(talkerID.isValid())
{
return talkerID.getValue();
}
}
}
return "";
}
/**
* Creates a formatted string with the TO identifiers or uses a default of zero (0)
*
*/
private static String getTo(AudioRecording audioRecording)
private String getTo(AudioRecording audioRecording)
{
Identifier identifier = audioRecording.getIdentifierCollection().getToIdentifier();
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
if(identifier != null)
{
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
}
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
AliasList aliasList = mAliasModel.getAliasList(audioRecording.getIdentifierCollection());
if(aliasList != null)
{
List<Alias> aliases = aliasList.getAliases(identifier);
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
return String.valueOf(streamAs.get().getStreamTalkgroupAlias().getValue());
}
}
if(identifier instanceof PatchGroupIdentifier patchGroupIdentifier)
{
return patchGroupIdentifier.getValue().getPatchGroup().getValue().toString();
}
else if(identifier instanceof TalkgroupIdentifier talkgroupIdentifier)
{
return String.valueOf(RadioReferenceDecoder.convertToRadioReferenceTalkgroup(talkgroupIdentifier.getValue(),
talkgroupIdentifier.getProtocol()));
}
else if(identifier instanceof RadioIdentifier radioIdentifier)
{
return radioIdentifier.getValue().toString();
}
}
return "0";

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,17 @@ import io.github.dsheirer.identifier.IdentifierClass;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.util.ThreadPool;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
@@ -41,17 +52,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV1BroadcastMetadataUpdater.class);
@@ -212,10 +212,20 @@ public class ShoutcastV1BroadcastMetadataUpdater implements IBroadcastMetadataUp
if(to != null)
{
sb.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
sb.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
}
else
{
sb.append("TO:").append(to);
}
if(!aliases.isEmpty())
{
sb.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -49,6 +49,15 @@ import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.Role;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.util.ThreadPool;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.math3.util.FastMath;
import org.apache.mina.core.RuntimeIoException;
import org.apache.mina.core.future.ConnectFuture;
@@ -59,15 +68,6 @@ import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcaster implements IBroadcastMetadataUpdater
{
private final static Logger mLog = LoggerFactory.getLogger(ShoutcastV2AudioStreamingBroadcaster.class);
@@ -303,13 +303,23 @@ public class ShoutcastV2AudioStreamingBroadcaster extends AudioStreamingBroadcas
if(to != null)
{
sbTitle2.append("TO:").append(to);
List<Alias> aliases = aliasList.getAliases(to);
if(!aliases.isEmpty())
//Check for 'Stream As Talkgroup' alias and use this instead of the decoded TO value.
Optional<Alias> streamAs = aliases.stream().filter(alias -> alias.getStreamTalkgroupAlias() != null).findFirst();
if(streamAs.isPresent())
{
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
sbTitle2.append("TO:").append(streamAs.get().getStreamTalkgroupAlias().getValue());
}
else
{
sbTitle2.append("TO:").append(to);
if(!aliases.isEmpty())
{
sbTitle2.append(" ").append(Joiner.on(", ").skipNulls().join(aliases));
}
}
}
else

View File

@@ -35,9 +35,12 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import jmbe.iface.IAudioCodec;
import jmbe.iface.IAudioCodecLibrary;
import jmbe.iface.IAudioCodecLibraryV2;
import jmbe.iface.IAudioCodecV2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,7 +50,7 @@ import org.slf4j.LoggerFactory;
public abstract class JmbeAudioModule extends AbstractAudioModule implements Listener<IMessage>, IMessageListener,
ISquelchStateListener
{
private static final Logger mLog = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final Logger LOG = LoggerFactory.getLogger(JmbeAudioModule.class);
private static final String JMBE_AUDIO_LIBRARY = "JMBE";
private static final List<String> mLibraryLoadStatusLogged = new ArrayList<>();
private IAudioCodec mAudioCodec;
@@ -56,12 +59,43 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
public JmbeAudioModule(UserPreferences userPreferences, AliasList aliasList, int timeslot)
{
super(aliasList, timeslot, DEFAULT_SEGMENT_AUDIO_SAMPLE_LENGTH);
super(aliasList, timeslot, DEFAULT_MAX_SEGMENT_LENGTH_MILLIS);
mUserPreferences = userPreferences;
MyEventBus.getGlobalEventBus().register(this);
loadConverter();
}
/**
* List of features supported by the JMBE audio library. Note: features are only supported in Version 2.0+
* @return list of features or an empty list if the current library version doesn't support features.
*/
public List<String> getFeatures()
{
if(mAudioCodec instanceof IAudioCodecV2 v2)
{
return v2.getFeatures();
}
return Collections.emptyList();
}
/**
* Sets the JMBE feature
* @param name of the feature
* @param value for the feature
*/
public void setFeature(String name, Object value)
{
if(mAudioCodec instanceof IAudioCodecV2 v2)
{
v2.setFeature(name, value);
}
else
{
LOG.info("Feature [" + name + "] is not supported by the current JMBE audio codec library version");
}
}
@Override
protected void closeAudioSegment()
{
@@ -135,7 +169,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("Loading JMBE library from [" + path + "]");
LOG.info("Loading JMBE library from [" + path + "]");
}
try
@@ -143,13 +177,32 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
URLClassLoader childClassLoader = new URLClassLoader(new URL[]{path.toUri().toURL()},
this.getClass().getClassLoader());
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
try
{
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibraryV2", true, childClassLoader);
}
catch(ClassNotFoundException legacyLibrary)
{
try
{
sLoadedJmbeAudioConverterClass = Class.forName("jmbe.JMBEAudioLibrary", true, childClassLoader);
}
catch(ClassNotFoundException missingLibrary)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
LOG.error("Couldn't load JMBE audio conversion library from [" + path +
"] - neither V2 nor V1 library class was found");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
}
catch(IllegalArgumentException iae)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY + getCodecName()))
{
mLog.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
LOG.error("Couldn't load JMBE audio conversion library - " + iae.getMessage());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY + getCodecName());
}
}
@@ -157,18 +210,11 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
catch(ClassNotFoundException e1)
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - class not found");
LOG.error("Couldn't load JMBE audio conversion library from path [" + path + "]");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
}
}
@@ -183,11 +229,23 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
if((library.getMajorVersion() == 1 && library.getMinorVersion() >= 0 &&
library.getBuildVersion() >= 0) || library.getMajorVersion() >= 1)
{
audioConverter = library.getAudioConverter(getCodecName());
audioConverter = library instanceof IAudioCodecLibraryV2 libraryV2 ?
libraryV2.getAudioConverterV2(getCodecName()) :
library.getAudioConverter(getCodecName());
String codecStatusKey = JMBE_AUDIO_LIBRARY + ":codec:" + getCodecName();
if(!mLibraryLoadStatusLogged.contains(codecStatusKey))
{
LOG.info("JMBE codec [" + getCodecName() + "] class [" +
audioConverter.getClass().getName() + "] features [" +
(audioConverter instanceof IAudioCodecV2 v2 ?
v2.getFeatures() : Collections.emptyList()) + "]");
mLibraryLoadStatusLogged.add(codecStatusKey);
}
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("JMBE audio conversion library loaded: " + library.getVersion());
LOG.info("JMBE audio conversion library loaded: " + library.getVersion());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
@@ -195,7 +253,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.warn("JMBE library version 1.0.0 or higher is required - found: " + library.getVersion());
LOG.warn("JMBE library version 1.0.0 or higher is required - found: " + library.getVersion());
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
@@ -204,7 +262,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.info("JMBE audio conversion library NOT FOUND");
LOG.info("JMBE audio conversion library NOT FOUND");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
@@ -213,7 +271,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - invocation target exception", ite);
LOG.error("Couldn't load JMBE audio conversion library - invocation target exception", ite);
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
@@ -221,7 +279,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - instantiation exception", e1);
LOG.error("Couldn't load JMBE audio conversion library - instantiation exception", e1);
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
@@ -229,7 +287,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - security restrictions");
LOG.error("Couldn't load JMBE audio conversion library - security restrictions");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
@@ -237,7 +295,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.error("Couldn't load JMBE audio conversion library - no such method exception");
LOG.error("Couldn't load JMBE audio conversion library - no such method exception");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}
@@ -246,7 +304,7 @@ public abstract class JmbeAudioModule extends AbstractAudioModule implements Lis
{
if(!mLibraryLoadStatusLogged.contains(JMBE_AUDIO_LIBRARY))
{
mLog.warn("JMBE audio library path is NOT SET in your User Preferences.");
LOG.warn("JMBE audio library path is NOT SET in your User Preferences.");
mLibraryLoadStatusLogged.add(JMBE_AUDIO_LIBRARY);
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -26,7 +26,6 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonRootName;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.encryption.EncryptionKey;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import java.util.ArrayList;
import java.util.List;
@@ -36,7 +35,7 @@ import java.util.List;
* radio identifiers.
*/
@JsonRootName("mbe_call")
@JsonPropertyOrder({"protocol", "call_type", "from", "to", "encrypted", "frames"})
@JsonPropertyOrder({"protocol", "version", "call_type", "from", "to", "encrypted", "system", "site", "frames"})
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MBECallSequence
{
@@ -77,6 +76,15 @@ public class MBECallSequence
mProtocol = protocol;
}
/**
* Gets the version number for the mbe file structure.
* Should be incremented only on breaking structural changes, not minor additions.
*/
@JsonProperty("version")
public Integer getVersion()
{
return 2;
}
/**
* Indicates if this sequences contains any audio frames
@@ -279,10 +287,18 @@ public class MBECallSequence
}
else
{
mVoiceFrames.add(new VoiceFrame(timestamp, frame));
add(new VoiceFrame(timestamp, frame));
}
}
/**
* Adds the voice frame to the sequence
*/
public void add(VoiceFrame voiceFrame)
{
mVoiceFrames.add(voiceFrame);
}
/**
* Adds an encrypted audio frame to this call sequence
*
@@ -308,6 +324,6 @@ public class MBECallSequence
*/
public void addEncryptedVoiceFrame(long timestamp, String frame, int algorithm, int keyid, String messageIndicator)
{
mVoiceFrames.add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
add(new VoiceFrame(timestamp, frame, algorithm, keyid, messageIndicator));
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -25,7 +25,6 @@ import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.module.decode.p25.audio.P25P1AudioModule;
import io.github.dsheirer.module.decode.p25.audio.P25P1CallSequenceRecorder;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import io.github.dsheirer.module.decode.p25.identifier.radio.APCO25RadioIdentifier;
import io.github.dsheirer.module.decode.p25.identifier.talkgroup.APCO25Talkgroup;
import io.github.dsheirer.preference.UserPreferences;

View File

@@ -1,23 +1,20 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2019 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.codec.mbe;
@@ -25,7 +22,6 @@ package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Joiner;
import io.github.dsheirer.module.decode.p25.audio.VoiceFrame;
import java.io.IOException;
import java.nio.file.Files;

View File

@@ -1,20 +1,23 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2019 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.module.decode.p25.audio;
package io.github.dsheirer.audio.codec.mbe;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -27,7 +30,7 @@ import com.fasterxml.jackson.annotation.JsonRootName;
*/
@JsonRootName(value = "frame")
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
@JsonPropertyOrder({"tag", "encryption_algorithm", "encryption_key_id", "encryption_mi", "time", "hex"})
public class VoiceFrame
{
private long mTimestamp;
@@ -35,6 +38,7 @@ public class VoiceFrame
private Integer mAlgorithm;
private Integer mKeyId;
private String mMessageIndicator;
private String mTag;
public VoiceFrame()
{
@@ -70,6 +74,24 @@ public class VoiceFrame
mMessageIndicator = messageIndicator;
}
/**
* Tag value for the voice frame
*/
@JsonProperty("tag")
public String getTag()
{
return mTag;
}
/**
* Sets the optional tag value for the frame
* @param tag to set
*/
public void setTag(String tag)
{
mTag = tag;
}
/**
* Timestamp the frame was transmitted
*

View File

@@ -0,0 +1,663 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import java.util.Arrays;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages the incoming audio segments and provides access to audio buffers from those segments.
*/
public class AudioChannel implements Listener<IdentifierUpdateNotification>
{
/**
* Quantity of audio samples to deliver per getAudio() method invocation, equivalent to 20 milliseconds at an 8 kHz
* sample rate.
*/
public static final int SAMPLES_PER_INTERVAL = 160;
/**
* Quantity of consecutive intervals without new audio (1 second) before an incomplete audio segment is treated as
* stalled and abandoned. An audio segment is delivered once to a single channel, so abandoning a segment that is
* still live loses the rest of the call. This has to tolerate decoder gaps, such as a missed P25 LDU (180 ms).
*/
static final int MAX_STALLED_INTERVALS = 50;
private static final Logger LOGGER = LoggerFactory.getLogger(AudioChannel.class);
private final AudioBuffer mAudioBuffer = new AudioBuffer();
private final Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
private final LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private final UserPreferences mUserPreferences;
private final String mChannelName;
private AudioSegment mCurrentAudioSegment;
private Listener<IdentifierCollection> mIdentifierCollectionListener;
private boolean mDropDuplicates;
private boolean mMetadataSent = false;
private boolean mMuted;
private boolean mDisabled;
private float[] mAudioSegmentStartTone;
private float[] mAudioSegmentDropTone;
private int mCurrentBufferIndex = -1;
private int mNoAudioFromSegmentIntervalCount = 0;
/**
* Constructs an instance
*/
public AudioChannel(UserPreferences userPreferences, String channelName)
{
mUserPreferences = userPreferences;
mChannelName = channelName;
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
updateToneInsertionAudioClips();
//Register to receive preference update notifications so we can update the preference items
MyEventBus.getGlobalEventBus().register(this);
}
/**
* Enables or disables this audio channel.
* @param disabled state
*/
public void setDisabled(boolean disabled)
{
mDisabled = disabled;
if(disabled)
{
AudioSegment segment = mAudioSegmentQueue.poll();
while(segment != null)
{
segment.decrementConsumerCount();
segment = mAudioSegmentQueue.poll();
}
}
}
/**
* Indicates if this audio channel is enabled.
*/
public boolean isDisabled()
{
return mDisabled;
}
/**
* Switches this channel's muted state
* @param muted true to mute and false to unmute
*/
public void setMuted(boolean muted)
{
mMuted = muted;
notify(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED);
}
/**
* Indicates if this channel is muted
* @return muted state
*/
public boolean isMuted()
{
return mMuted;
}
/**
* Plays the test audio sample data at higher priority that current audio queue.
* @param samples of test audio to play.
*/
public void playTest(float[] samples)
{
if(isDisabled())
{
return;
}
mAudioBuffer.insert(samples);
}
/**
* Indicates if this channel has an audio segment, indicating that it is right in the middle of playback and
* the audio output should temporarily pause until more audio is available from the segment.
* @return true if this channel is processing a non-complete audio segment.
*/
public boolean hasAudioSegment()
{
return mCurrentAudioSegment != null;
}
/**
* Broadcasts a null identifier collection to the UI to clear any displayed metadata (TO/FROM) identifiers
*/
public void clearMetadata()
{
if(mMetadataSent)
{
broadcast(null);
mMetadataSent = false;
}
}
/**
* Provides audio for this channel from the audio segment queue.
* @return audio or null if there are no audio segments or test audio to play.
*/
public float[] getAudio()
{
if(mAudioBuffer.isFull())
{
float[] audio = mAudioBuffer.get();
if(isMuted())
{
return new float[SAMPLES_PER_INTERVAL];
}
else
{
return audio;
}
}
if(mCurrentAudioSegment == null)
{
loadNextAudioSegment();
}
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
while(isThrowaway(mCurrentAudioSegment))
{
if(mCurrentBufferIndex > 0)
{
//Flush the buffer and insert the drop tone
mAudioBuffer.replace(mAudioSegmentDropTone);
}
disposeCurrentAudioSegment();
loadNextAudioSegment();
}
if(mCurrentAudioSegment != null)
{
mNoAudioFromSegmentIntervalCount++;
}
while(!mAudioBuffer.isFull() && mCurrentAudioSegment != null &&
mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
{
if(mCurrentBufferIndex < 0)
{
mAudioBuffer.add(mAudioSegmentStartTone);
mAudioBuffer.add(mCurrentAudioSegment.getAudioBuffer(0));
mCurrentBufferIndex = 1;
broadcast(mCurrentAudioSegment.getIdentifierCollection());
mMetadataSent = true;
}
else
{
mAudioBuffer.add(mCurrentAudioSegment.getAudioBuffer(mCurrentBufferIndex++));
}
mNoAudioFromSegmentIntervalCount = 0;
}
//Dispose completed/fully-played audio segments and segments that have stalled/failed to progress
if(mCurrentAudioSegment != null && ((mNoAudioFromSegmentIntervalCount >= MAX_STALLED_INTERVALS) ||
(mCurrentAudioSegment.isComplete() && (mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))))
{
disposeCurrentAudioSegment();
mNoAudioFromSegmentIntervalCount = 0;
}
float[] audio = null;
if(mAudioBuffer.isFull())
{
audio = mAudioBuffer.get();
}
else if(mCurrentAudioSegment == null && !mAudioBuffer.isEmpty())
{
audio = mAudioBuffer.flush();
}
//Finally, if we have buffer audio and we're muted, return silence
if(isMuted() && audio != null)
{
audio = new float[SAMPLES_PER_INTERVAL];
}
return audio;
}
/**
* Notifies listener(s) of the audio event.
* @param eventType to notify for this channel
*/
public void notify(AudioEvent.Type eventType)
{
broadcastAudioEvent(new AudioEvent(eventType, getChannelName()));
}
/**
* Registers a single listener to receive audio start and audio stop events
*/
public void addAudioEventListener(Listener<AudioEvent> listener)
{
mAudioEventBroadcaster.addListener(listener);
}
/**
* Removes the audio event listener
* @param listener to remove
*/
public void removeAudioEventListener(Listener<AudioEvent> listener)
{
mAudioEventBroadcaster.removeListener(listener);
}
/**
* Broadcasts an audio event to the registered listener
*/
private void broadcastAudioEvent(AudioEvent audioEvent)
{
mAudioEventBroadcaster.broadcast(audioEvent);
}
/**
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
* @param audioSegment to evaluate
* @return true if the audio should be thrown away.
*/
private boolean isThrowaway(AudioSegment audioSegment)
{
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
}
/**
* Channel name
*/
public String getChannelName()
{
return mChannelName;
}
/**
* Broadcasts audio identifier collection metadata to the registered listener
*/
private void broadcast(IdentifierCollection identifierCollection)
{
if(mIdentifierCollectionListener != null)
{
mIdentifierCollectionListener.receive(identifierCollection);
}
}
/**
* Registers a single listener to receive the audio metadata from each
* audio packet
*/
public void setIdentifierCollectionListener(Listener<IdentifierCollection> listener)
{
mIdentifierCollectionListener = listener;
}
/**
* Unregisters the current audio metadata listener
*/
public void removeAudioMetadataListener()
{
mIdentifierCollectionListener = null;
}
/**
* Prepares this audio output for disposal.
*/
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
disposeCurrentAudioSegment();
mAudioEventBroadcaster.clear();
mIdentifierCollectionListener = null;
}
/**
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
{
if(preferenceType == PreferenceType.PLAYBACK)
{
updateToneInsertionAudioClips();
}
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
{
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
}
}
/**
* Indicates if the audio segment is linked to the current playback audio segment
*/
public boolean isLinkedTo(AudioSegment audioSegment)
{
return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment);
}
/**
* Receive and process audio identifier update notifications.
*/
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
{
if(mCurrentAudioSegment != null)
{
IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection();
if(identifierCollection != null)
{
broadcast(identifierCollection);
}
}
}
/**
* Updates audio segment start, drop and preempt tone insertion clips
*/
private void updateToneInsertionAudioClips()
{
mAudioSegmentStartTone = mUserPreferences.getPlaybackPreference().getStartTone();
mAudioSegmentDropTone = mUserPreferences.getPlaybackPreference().getDropTone();;
}
/**
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
* @return true if empty
*/
public boolean isEmpty()
{
return mAudioSegmentQueue.isEmpty();
}
/**
* Next audio segment queued for playback, without removing it from the queue.
* @return queued audio segment or null if the queue is empty.
*/
AudioSegment peekQueuedAudioSegment()
{
return mAudioSegmentQueue.peek();
}
/**
* Removes the next audio segment from the queue, transferring it (and its consumer count) to the caller.
* @return queued audio segment or null if the queue is empty.
*/
AudioSegment pollQueuedAudioSegment()
{
return mAudioSegmentQueue.poll();
}
/**
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
* calling entity.
*
* @param audioSegment to schedule for playback.
*/
public void play(AudioSegment audioSegment)
{
if(audioSegment != null)
{
//Audio segment user count has already been incremented by the external caller.
if(isDisabled())
{
audioSegment.decrementConsumerCount();
}
else
{
mAudioSegmentQueue.add(audioSegment);
}
}
}
/**
* Dispose of the currently assigned audio segment.
*/
private void disposeCurrentAudioSegment()
{
if(mCurrentAudioSegment != null)
{
mCurrentAudioSegment.decrementConsumerCount();
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
mCurrentAudioSegment = null;
}
}
/**
* Loads the next audio segment from the queue.
*/
private void loadNextAudioSegment()
{
AudioSegment audioSegment = mAudioSegmentQueue.poll();
boolean verificationInProgress = (audioSegment != null);
while(verificationInProgress)
{
if(audioSegment != null)
{
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
if(isThrowaway(audioSegment))
{
audioSegment.decrementConsumerCount();
audioSegment = mAudioSegmentQueue.poll();
}
else
{
verificationInProgress = false;
}
}
else
{
verificationInProgress = false;
}
}
mCurrentAudioSegment = audioSegment;
if(audioSegment != null)
{
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
mCurrentBufferIndex = -1;
mNoAudioFromSegmentIntervalCount = 0;
}
/**
* Manages a stream of float PCM audio samples. Employs thread locking of the audio buffer to support both the
* audio processor thread and user initiated test audio replay.
*/
static class AudioBuffer
{
private final Lock mLock = new ReentrantLock();
private float[] mBuffer = new float[0];
/**
* Indicates if the buffer has at least one interval of samples available
*/
public boolean isFull()
{
return mBuffer.length >= SAMPLES_PER_INTERVAL;
}
/**
* Indicates if the buffer is empty
*/
public boolean isEmpty()
{
return mBuffer.length == 0;
}
/**
* Appends the audio to the buffer
* @param audio to add
*/
public void add(float[] audio)
{
if(audio != null)
{
mLock.lock();
try
{
mBuffer = concatenate(mBuffer, audio);
}
finally
{
mLock.unlock();
}
}
}
/**
* Inserts the audio to the front of the buffer. This can be used to insert test audio ahead of any queued audio.
* @param audio to insert
*/
public void insert(float[] audio)
{
if(audio != null)
{
mLock.lock();
try
{
mBuffer = concatenate(audio, mBuffer);
}
finally
{
mLock.unlock();
}
}
}
/**
* Replaces the content in the buffer with the audio argument
* @param audio to replace into the buffer
*/
public void replace(float[] audio)
{
if(audio != null)
{
mLock.lock();
try
{
mBuffer = audio;
}
finally
{
mLock.unlock();
}
}
}
/**
* Concatenate two arrays. Note: this does not null check either array.
* @param a first array
* @param b second array
* @return concatenated array
*/
private static float[] concatenate(float[] a, float[] b)
{
a = Arrays.copyOf(a, a.length + b.length);
System.arraycopy(b, 0, a, a.length - b.length, b.length);
return a;
}
/**
* Retrieves an array of 160 samples if available, otherwise returns null.
* @return 160 samples or null.
*/
public float[] get()
{
if(mBuffer.length >= SAMPLES_PER_INTERVAL)
{
mLock.lock();
try
{
float[] fragment = Arrays.copyOf(mBuffer, SAMPLES_PER_INTERVAL);
mBuffer = Arrays.copyOfRange(mBuffer, SAMPLES_PER_INTERVAL, mBuffer.length);
return fragment;
}
finally
{
mLock.unlock();
}
}
return null;
}
/**
* Flushes remaining samples from the buffer when the quantity is less than a full sample set.
* @return remaining samples or null.
*/
public float[] flush()
{
mLock.lock();
try
{
if(mBuffer.length > 0)
{
float[] fragment = Arrays.copyOf(mBuffer, SAMPLES_PER_INTERVAL);
if(mBuffer.length > SAMPLES_PER_INTERVAL)
{
mBuffer = Arrays.copyOfRange(mBuffer, SAMPLES_PER_INTERVAL, mBuffer.length);
}
else
{
mBuffer = new float[0];
}
return fragment;
}
return null;
}
finally
{
mLock.unlock();
}
}
}
}

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.audio.playback;
@@ -40,13 +39,6 @@ import io.github.dsheirer.settings.ColorSetting;
import io.github.dsheirer.settings.Setting;
import io.github.dsheirer.settings.SettingChangeListener;
import io.github.dsheirer.settings.SettingsManager;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
@@ -54,7 +46,17 @@ import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
/**
* UI to wrap an audio channel and provide display of metadata and playback state information.
*/
public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, SettingChangeListener
{
private static final long serialVersionUID = 1L;
@@ -66,30 +68,36 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
public static final String PROPERTY_COLOR_MUTED = PROPERTY_PREFIX + "muted";
public static final String PROPERTY_COLOR_VALUE = PROPERTY_PREFIX + "value";
private Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
private Color mBackgroundColor;
private Color mLabelColor;
private Color mMutedColor;
private Color mValueColor;
private IconModel mIconModel;
private SettingsManager mSettingsManager;
private UserPreferences mUserPreferences;
private TalkgroupFormatPreference mTalkgroupFormatPreference;
private AudioOutput mAudioOutput;
private JLabel mMutedLabel = new JLabel("M");
private JLabel mChannelName = new JLabel(" ");
private JLabel mIconLabel = new JLabel(" ");
private JLabel mIdentifierLabel = new JLabel("-----");
private final AudioChannel mAudioChannel;
private final AliasModel mAliasModel;
private final IconModel mIconModel;
private final SettingsManager mSettingsManager;
private final UserPreferences mUserPreferences;
private final TalkgroupFormatPreference mTalkgroupFormatPreference;
private Identifier mIdentifier;
private List<Alias> mAliases = Collections.EMPTY_LIST;
private AliasModel mAliasModel;
private Lock mLock = new ReentrantLock();
private final Lock mLock = new ReentrantLock();
public AudioChannelPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
AudioOutput audioOutput, AliasModel aliasModel)
private final Font mFont = new Font(Font.MONOSPACED, Font.PLAIN, 16);
private final Color mBackgroundColor;
private final Color mLabelColor;
private final Color mMutedColor;
private final Color mValueColor;
private final JLabel mMutedLabel = new JLabel("M");
private JLabel mChannelName = new JLabel(" ");
private final JLabel mIconLabel = new JLabel(" ");
private final JLabel mIdentifierLabel = new JLabel("-----");
/**
* Constructs an instance
* @param audioChannel to wrap by this panel
* @param aliasModel for alias lookup
* @param iconModel for icon lookup
* @param settingsManager for monitoring changes to tone insertion
* @param userPreferences for lookup of tone and other preferences
*/
public AudioChannelPanel(AudioChannel audioChannel, AliasModel aliasModel, IconModel iconModel,
SettingsManager settingsManager, UserPreferences userPreferences)
{
mIconModel = iconModel;
mSettingsManager = settingsManager;
@@ -97,12 +105,12 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mTalkgroupFormatPreference = mUserPreferences.getTalkgroupFormatPreference();
mAudioOutput = audioOutput;
mAudioChannel = audioChannel;
if(mAudioOutput != null)
if(mAudioChannel != null)
{
mAudioOutput.addAudioEventListener(this);
mAudioOutput.setIdentifierCollectionListener(new AudioMetadataProcessor());
mAudioChannel.addAudioEventListener(this);
mAudioChannel.setIdentifierCollectionListener(new AudioMetadataProcessor());
}
mBackgroundColor = SystemProperties.getInstance().get(PROPERTY_COLOR_BACKGROUND, Color.BLACK);
@@ -131,10 +139,10 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
//Deregister from receiving preference update notifications
MyEventBus.getGlobalEventBus().unregister(this);
if(mAudioOutput != null)
if(mAudioChannel != null)
{
mAudioOutput.removeAudioEventListener(this);
mAudioOutput.removeAudioMetadataListener();
mAudioChannel.removeAudioEventListener(this);
mAudioChannel.removeAudioMetadataListener();
}
}
@@ -152,7 +160,7 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
mMutedLabel.setVisible(false);
add(mMutedLabel);
mChannelName = new JLabel(mAudioOutput != null ? mAudioOutput.getChannelName() : " ");
mChannelName = new JLabel(mAudioChannel != null ? mAudioChannel.getChannelName() : " ");
mChannelName.setFont(mFont);
mChannelName.setForeground(mLabelColor);
add(mChannelName);
@@ -172,25 +180,11 @@ public class AudioChannelPanel extends JPanel implements Listener<AudioEvent>, S
switch(audioEvent.getType())
{
case AUDIO_STOPPED:
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
resetLabels();
}
});
EventQueue.invokeLater(this::resetLabels);
break;
case AUDIO_MUTED:
case AUDIO_UNMUTED:
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
mMutedLabel.setVisible(mAudioOutput.isMuted());
}
});
EventQueue.invokeLater(() -> mMutedLabel.setVisible(mAudioChannel.isMuted()));
break;
default:
break;

View File

@@ -1,7 +1,6 @@
/*
* ******************************************************************************
* sdrtrunk
* Copyright (C) 2014-2019 Dennis Sheirer
* *****************************************************************************
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -15,7 +14,7 @@
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* *****************************************************************************
* ****************************************************************************
*/
package io.github.dsheirer.audio.playback;
@@ -24,18 +23,26 @@ import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.settings.SettingsManager;
import java.awt.Color;
import java.awt.Component;
import net.miginfocom.swing.MigLayout;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import java.awt.Color;
import java.awt.Component;
import java.util.List;
/**
* Displays one or more audio channel panels.
*/
public class AudioChannelsPanel extends JPanel
{
private static final long serialVersionUID = 1L;
/**
* Constructs an instance
* @param iconModel for icon access
* @param userPreferences to monitor for audio playback changes
* @param settingsManager for tone insertion settings
* @param controller for the audio channels
* @param aliasModel for accessing aliases
*/
public AudioChannelsPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
IAudioController controller, AliasModel aliasModel)
{
@@ -44,15 +51,13 @@ public class AudioChannelsPanel extends JPanel
setBackground(Color.BLACK);
List<AudioOutput> outputs = controller.getAudioOutputs();
addSeparator();
for(int x = 0; x < outputs.size(); x++)
for(int x = 0; x < controller.getAudioChannels().size(); x++)
{
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, outputs.get(x), aliasModel));
add(new AudioChannelPanel(controller.getAudioChannels().get(x), aliasModel, iconModel, settingsManager, userPreferences));
if(x < outputs.size() - 1)
if(x < controller.getAudioChannels().size() - 1)
{
addSeparator();
}
@@ -60,10 +65,10 @@ public class AudioChannelsPanel extends JPanel
/* Add an empty channel panel so that the panel is sized appropriately
* for either a single channel or two channels */
if(outputs.size() == 1)
if(controller.getAudioChannels().size() == 1)
{
addSeparator();
add(new AudioChannelPanel(iconModel, userPreferences, settingsManager, null, aliasModel), "growx");
add(new AudioChannelPanel(null, aliasModel, iconModel, settingsManager, userPreferences), "growx");
}
}
@@ -81,6 +86,9 @@ public class AudioChannelsPanel extends JPanel
}
}
/**
* Adds a separator to this panel.
*/
private void addSeparator()
{
JSeparator separator = new JSeparator(JSeparator.VERTICAL);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,22 +18,14 @@
*/
package io.github.dsheirer.audio.playback;
import com.google.common.eventbus.Subscribe;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioSegment;
import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.identifier.IdentifierCollection;
import io.github.dsheirer.identifier.IdentifierUpdateNotification;
import io.github.dsheirer.log.LoggingSuppressor;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.preference.playback.PlayTestAudioRequest;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -41,11 +33,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.BooleanControl;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Control;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.Line;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
@@ -56,498 +47,214 @@ import javax.sound.sampled.SourceDataLine;
* Audio output/playback channel for a single audio mixer channel. Providers support for playback of audio segments
* and broadcasts audio segment metadata to registered listeners (ie gui components).
*/
public abstract class AudioOutput implements LineListener, Listener<IdentifierUpdateNotification>
public class AudioOutput implements LineListener
{
private final static Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
private static final Logger mLog = LoggerFactory.getLogger(AudioOutput.class);
private static final LoggingSuppressor LOGGING_SUPPRESSOR = new LoggingSuppressor(mLog);
private int mBufferStartThreshold;
private int mBufferStopThreshold;
private Listener<IdentifierCollection> mIdentifierCollectionListener;
private Broadcaster<AudioEvent> mAudioEventBroadcaster = new Broadcaster<>();
private SourceDataLine mOutput;
private Mixer mMixer;
private MixerChannel mMixerChannel;
private static final int BUFFER_SIZE_SAMPLES = 1000; //At 8 kHz audio
private final AudioPlaybackDeviceDescriptor mAudioPlaybackDeviceDescriptor;
private final AudioProvider mAudioProvider;
private FloatControl mGainControl;
private BooleanControl mMuteControl;
private AudioEvent mAudioStartEvent;
private AudioEvent mAudioStopEvent;
private boolean mCanProcessAudio = false;
private LinkedTransferQueue<AudioSegment> mAudioSegmentQueue = new LinkedTransferQueue<>();
private AudioSegment mCurrentAudioSegment;
private int mCurrentBufferIndex = 0;
private UserPreferences mUserPreferences;
private ByteBuffer mAudioSegmentStartTone;
private ByteBuffer mAudioSegmentDropTone;
private boolean mRunning = false;
private ScheduledExecutorService mScheduledExecutorService;
private ScheduledFuture<?> mProcessorFuture;
private boolean mDropDuplicates;
private long mOutputLastTimestamp = 0;
private static final long STALE_PLAYBACK_THRESHOLD_MS = 500;
private AudioFormat mAudioFormat;
private Line.Info mLineInfo;
private int mRequestedBufferSize;
private SourceDataLine mSourceDataLine;
private boolean mCanProcessAudio = false;
private boolean mRunning = false;
/**
* Single audio channel playback with automatic starting and stopping of the
* underlying sourcedataline specified by the mixer and mixer channel
* arguments.
* Audio output for the selected audio playback device and provider. Opens a SourceDataLine from the miser for the
* audio format specified in the descriptor. Employs a single threaded executor to process audio 10x a second and
* manages the start/stop control for the dataline based on audio availability.
*
* Maintains an internal non-blocking audio packet queue and processes this
* queue 25 times a second (every 40 ms).
*
* @param mixer to obtain source data line
* @param mixerChannel either mono or left/right stereo
* @param audioFormat to use during playback
* @param lineInfo to use when obtaining the source data line
* @param requestedBufferSize of approximately 1 second of audio
* @param descriptor for the mixer and audio format
* @param audioProvider for access to audio from audio segments
*/
public AudioOutput(Mixer mixer, MixerChannel mixerChannel, AudioFormat audioFormat, Line.Info lineInfo,
int requestedBufferSize, UserPreferences userPreferences)
public AudioOutput(AudioPlaybackDeviceDescriptor descriptor, AudioProvider audioProvider)
{
mMixer = mixer;
mMixerChannel = mixerChannel;
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + mixerChannel.name()));
mUserPreferences = userPreferences;
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
mAudioFormat = audioFormat;
mLineInfo = lineInfo;
mRequestedBufferSize = requestedBufferSize;
mAudioProvider = audioProvider;
Mixer mixer = AudioSystem.getMixer(descriptor.getMixerInfo());
try
if(mixer == null)
{
mOutput = (SourceDataLine) mMixer.getLine(mLineInfo);
List<AudioPlaybackDeviceDescriptor> descriptors = AudioPlaybackDeviceManager.getAudioPlaybackDevices();
if(mOutput != null)
int selected = 0;
while((mixer == null) && (selected < descriptors.size()))
{
mOutput.open(audioFormat, requestedBufferSize);
descriptor = descriptors.get(selected++);
mixer = AudioSystem.getMixer(descriptor.getMixerInfo());
}
}
//Start threshold: buffer is full with 10% or less of capacity remaining
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
mAudioPlaybackDeviceDescriptor = descriptor;
//Stop threshold: buffer is empty with 90% or more capacity available
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
if(mixer != null)
{
try
{
mSourceDataLine = (SourceDataLine) mixer.getLine(new DataLine.Info(SourceDataLine.class,
descriptor.getAudioFormat()));
mOutput.addLineListener(this);
if(mOutput != null)
if(mSourceDataLine != null)
{
mSourceDataLine.addLineListener(this);
try
{
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
Control gain = mSourceDataLine.getControl(FloatControl.Type.MASTER_GAIN);
mGainControl = (FloatControl) gain;
}
catch(IllegalArgumentException iae)
{
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
"MASTER GAIN control for stereo line [" + mixer.getMixerInfo().getName() + "]");
}
try
{
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
mMuteControl = (BooleanControl) mute;
}
catch(IllegalArgumentException iae)
{
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
"MUTE control for stereo line [" + mixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
//Run the queue processor task every 100 milliseconds or 10 times a second
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
mCanProcessAudio = true;
}
}
catch(LineUnavailableException e)
{
mLog.error("Couldn't open source data line for mixer: " + descriptor.getMixerInfo().getName(), e);
}
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
mCanProcessAudio = true;
if(mCanProcessAudio)
{
openSourceDataLine();
//The audio provider gives us audio 160 samples per interval representing 20 milliseconds of audio.
// Run the scheduled executor at just slightly faster than that time, recognizing that it will
// block against the mixer source data line until it can write all 160 samples.
mScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory(
"sdrtrunk audio output " + descriptor.getMixerInfo().getName()));
mProcessorFuture = mScheduledExecutorService.scheduleAtFixedRate(new AudioProcessor(),
0, 19, TimeUnit.MILLISECONDS);
}
else
{
//Disable each of the audio channels so they don't queue audio segments.
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
{
audioChannel.setDisabled(true);
}
}
}
}
/**
* Opens and starts the source data line.
*/
private void openSourceDataLine()
{
try
{
if(mSourceDataLine != null && !mSourceDataLine.isOpen())
{
int bufferSize = BUFFER_SIZE_SAMPLES * 2 * mAudioPlaybackDeviceDescriptor.getAudioFormat().getChannels();
mSourceDataLine.open(mAudioProvider.getAudioFormat(), bufferSize);
bufferSize = mSourceDataLine.getBufferSize();
//Fill the line with silence so we can start it
mSourceDataLine.write(new byte[bufferSize], 0, bufferSize);
mSourceDataLine.start();
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
{
audioChannel.setDisabled(false);
}
}
}
catch(LineUnavailableException e)
{
mLog.error("Couldn't obtain audio source data line for audio output - mixer [" +
mMixer.getMixerInfo().getName() + "]");
}
LOGGING_SUPPRESSOR.error("Can't open", 3, "Error opening source data line", e);
mRunning = false;
updateToneInsertionAudioClips();
//Register to receive preference update notifications so we can update the preference items
MyEventBus.getGlobalEventBus().register(this);
}
/**
* Indicates if this audio output doesn't currently have any audio segments queued for playback.
* @return true if empty
*/
public boolean isEmpty()
{
return mAudioSegmentQueue.isEmpty();
}
/**
* Schedules the audio segment for playback. The audio segment user count should already be incremented by the
* calling entity.
*
* @param audioSegment to schedule for playback.
*/
public void play(AudioSegment audioSegment)
{
if(audioSegment != null)
{
//Audio segment use count has already been incremented by the external caller.
mAudioSegmentQueue.add(audioSegment);
}
}
/**
* Indicates if the audio segment is linked to the current playback audio segment
*/
public boolean isLinkedTo(AudioSegment audioSegment)
{
return audioSegment.isLinked() && audioSegment.isLinkedTo(mCurrentAudioSegment);
}
/**
* Receive and process audio identifier update notifications.
*/
@Override
public void receive(IdentifierUpdateNotification identifierUpdateNotification)
{
if(mCurrentAudioSegment != null)
{
IdentifierCollection identifierCollection = mCurrentAudioSegment.getIdentifierCollection();
if(identifierCollection != null)
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
{
broadcast(identifierCollection);
audioChannel.setDisabled(true);
}
}
}
/**
* Guava event bus notifications that the preferences have been updated, so that we can update audio segment tones.
* Plays the test audio over the selected audio channel(s)
* @param request to play test audio
*/
@Subscribe
public void preferenceUpdated(PreferenceType preferenceType)
public void playTestAudio(PlayTestAudioRequest request)
{
if(preferenceType == PreferenceType.PLAYBACK)
//If we're not running, resize the audio samples to fill the buffer enough to trigger playback.
float[] audio = mRunning ? request.audio() : Arrays.copyOf(request.audio(), BUFFER_SIZE_SAMPLES);
List<AudioChannel> channels = mAudioProvider.getAudioChannels();
if(request.isAllChannels())
{
updateToneInsertionAudioClips();
}
else if(preferenceType == PreferenceType.DUPLICATE_CALL_DETECTION)
{
mDropDuplicates = mUserPreferences.getCallManagementPreference().isDuplicatePlaybackSuppressionEnabled();
}
}
/**
* Updates audio segment start, drop and preempt tone insertion clips
*/
private void updateToneInsertionAudioClips()
{
mAudioSegmentStartTone = null;
mAudioSegmentDropTone = null;
float[] start = mUserPreferences.getPlaybackPreference().getStartTone();
if(start != null)
{
mAudioSegmentStartTone = convert(start);
}
float[] drop = mUserPreferences.getPlaybackPreference().getDropTone();
if(drop != null)
{
mAudioSegmentDropTone = convert(drop);
}
}
/**
* Writes the audio buffer data to the source data line. If the data line is not currently playing, write as
* much of the buffer to the data line as will fit, start the dataline, and then finish writing the residual
* buffer content to the data line as a blocking call.
*
* @param buffer of audio to playback
*/
private void playAudio(ByteBuffer buffer)
{
if(mOutput == null)
{
LOGGING_SUPPRESSOR.error("null output", 2, "Audio Output is null - ignoring audio playback request");
return;
}
if(buffer != null)
{
int wrote = 0;
//If the output data line is not running, we can only write up to the available capacity. So, only write
//what will fit initially, start playback, and then use a blocking write for the remainder.
if(!mOutput.isRunning())
for(AudioChannel channel : channels)
{
int toWrite = mOutput.available();
if(toWrite > buffer.array().length)
{
toWrite = buffer.array().length;
}
//Top off the buffer and check if we can start it
wrote += mOutput.write(buffer.array(), 0, toWrite);
mOutputLastTimestamp = System.currentTimeMillis();
checkStart();
//Something is causing the source data line to fail to accept audio byte data via the write() method
//and this seems to have started around JDK22, maybe. We can detect when this happens and close() and
//reopen the data line to clear the error.
if(!mOutput.isRunning() && wrote <= 0 && buffer.array().length > 0)
{
mOutput.close();
try
{
mOutput = (SourceDataLine)mMixer.getLine(mLineInfo);
if(mOutput != null)
{
try
{
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
mGainControl = (FloatControl) gain;
}
catch(IllegalArgumentException iae)
{
LOGGING_SUPPRESSOR.error("no gain control", 2, "Couldn't obtain " +
"MASTER GAIN control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
try
{
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
mMuteControl = (BooleanControl) mute;
}
catch(IllegalArgumentException iae)
{
LOGGING_SUPPRESSOR.error("no mute control", 2, "Couldn't obtain " +
"MASTER MUTE control for stereo line [" + mMixer.getMixerInfo().getName() + " | " +
getChannelName() + "]");
}
LOGGING_SUPPRESSOR.info("reopen audio output success", 2,
"Closed and reopened audio output - success - mOutput is not null");
}
else
{
LOGGING_SUPPRESSOR.info("reopen audio output fail", 2,
"Closed and reopened audio output - fail - mOutput is null");
return;
}
}
catch(LineUnavailableException lue)
{
LOGGING_SUPPRESSOR.error("reopen fail for lua", 2, "Attempt to reopen " +
"audio source data line failed", lue);
return;
}
try
{
mOutput.open(mAudioFormat, mRequestedBufferSize);
//Start threshold: buffer is full with 10% or less of capacity remaining
mBufferStartThreshold = (int) (mOutput.getBufferSize() * 0.10);
//Stop threshold: buffer is empty with 90% or more capacity available
mBufferStopThreshold = (int) (mOutput.getBufferSize() * 0.90);
try
{
Control gain = mOutput.getControl(FloatControl.Type.MASTER_GAIN);
mGainControl = (FloatControl) gain;
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MASTER GAIN control for stereo line [" + getChannelName() +
"] after reopening the source data line");
}
try
{
Control mute = mOutput.getControl(BooleanControl.Type.MUTE);
mMuteControl = (BooleanControl) mute;
}
catch(IllegalArgumentException iae)
{
mLog.warn("Couldn't obtain MUTE control for stereo line [" + getChannelName() +
"] after reopening the source data line");
}
mAudioStartEvent = new AudioEvent(AudioEvent.Type.AUDIO_STARTED, getChannelName());
mAudioStopEvent = new AudioEvent(AudioEvent.Type.AUDIO_STOPPED, getChannelName());
mCanProcessAudio = true;
}
catch(LineUnavailableException e)
{
LOGGING_SUPPRESSOR.error("failed reopen source data line lua", 2,
"Failed to (re)open the source data line for audio output - mixer [" +
mMixer.getMixerInfo().getName() + "]");
return;
}
}
}
if(mOutput.isRunning() && wrote < buffer.array().length)
{
//This will block until the buffer is fully written to the data line
mOutput.write(buffer.array(), wrote, buffer.array().length - wrote);
mOutputLastTimestamp = System.currentTimeMillis();
channel.playTest(audio);
}
}
}
/**
* Dispose of the currently assigned audio segment.
*/
private void disposeCurrentAudioSegment()
{
if(mCurrentAudioSegment != null)
else if(request.channel() < channels.size())
{
mCurrentAudioSegment.decrementConsumerCount();
mCurrentAudioSegment.removeIdentifierUpdateNotificationListener(this);
mCurrentAudioSegment = null;
broadcast(null);
channels.get(request.channel()).playTest(audio);
}
else if(!channels.isEmpty())
{
channels.getFirst().playTest(audio);
}
else
{
LOGGING_SUPPRESSOR.info("No Audio Channels", 2,
"Unable to play test audio - no audio channels configured currently");
}
}
/**
* Loads the next audio segment from the queue.
* Audio playback device descriptor for this audio output
*/
private void loadNextAudioSegment()
public AudioPlaybackDeviceDescriptor getAudioPlaybackDeviceDescriptor()
{
AudioSegment audioSegment = mAudioSegmentQueue.poll();
boolean verificationInProgress = (audioSegment != null);
while(verificationInProgress)
{
if(audioSegment != null)
{
//Throw away the audio segment if it has been flagged as do not monitor or is duplicate
if(isThrowaway(audioSegment))
{
audioSegment.decrementConsumerCount();
audioSegment = mAudioSegmentQueue.poll();
}
else
{
verificationInProgress = false;
}
}
else
{
verificationInProgress = false;
}
}
mCurrentAudioSegment = audioSegment;
if(audioSegment != null)
{
mCurrentAudioSegment.addIdentifierUpdateNotificationListener(this);
broadcast(mCurrentAudioSegment.getIdentifierCollection());
}
mCurrentBufferIndex = 0;
return mAudioPlaybackDeviceDescriptor;
}
/**
* Indicates if the audio segment should be thrown away because it is marked as duplicate or do-not-monitor.
* @param audioSegment to evaluate
* @return true if the audio should be thrown away.
* Audio provider managed by this audio output
*/
private boolean isThrowaway(AudioSegment audioSegment)
public AudioProvider getAudioProvider()
{
return audioSegment != null && (audioSegment.isDoNotMonitor() || mDropDuplicates && (audioSegment.isDuplicate()));
return mAudioProvider;
}
/**
* Manage audio segment playback and process audio segment buffers. This method is designed to be called
* by a threaded processor repeatedly to playback the current audio segment and check for and start newly
* added audio segments. It also handles starting and stopping the playback source data line to avoid audio
* discontinuities due to buffer underruns.
* Process audio from the audio provider and writes it to the mixer's source data line. Calls to this method can
* block on the source data line until it has capacity available to accept the audio.
*/
private void processAudio()
{
if(mCurrentAudioSegment == null)
if(mSourceDataLine == null)
{
loadNextAudioSegment();
LOGGING_SUPPRESSOR.error("null output", 2,
"Audio Output source data line is null - ignoring audio playback request");
return;
}
//Evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
while(isThrowaway(mCurrentAudioSegment))
ByteBuffer buffer = mAudioProvider.getAudio();
if(buffer != null)
{
if(mCurrentBufferIndex > 0)
//This is a blocking method call.
int wrote = mSourceDataLine.write(buffer.array(), 0, buffer.array().length);
//Around JDK 22 something started causing the source data line to fail to accept audio byte data via the
//write() method. When this happens, close and reopen the data line to clear the error state.
if(wrote <= 0)
{
playAudio(mAudioSegmentDropTone);
}
disposeCurrentAudioSegment();
loadNextAudioSegment();
}
while(mCurrentAudioSegment != null && mCurrentBufferIndex < mCurrentAudioSegment.getAudioBufferCount())
{
//Continuously evaluate current audio segment to see if the status has changed for duplicate or do-not-monitor.
if(isThrowaway(mCurrentAudioSegment))
{
if(mCurrentBufferIndex > 0)
{
playAudio(mAudioSegmentDropTone);
}
disposeCurrentAudioSegment();
}
else
{
if(mCurrentBufferIndex == 0)
{
playAudio(mAudioSegmentStartTone);
}
try
{
float[] audioBuffer = mCurrentAudioSegment.getAudioBuffers().get(mCurrentBufferIndex++);
if(audioBuffer != null)
{
ByteBuffer audio = convert(audioBuffer);
//This call blocks until all audio bytes are dumped into the data line.
playAudio(audio);
}
}
catch(Exception e)
{
mLog.error("Error while processing audio for [" + mMixerChannel.name() + "]", e);
}
LOGGING_SUPPRESSOR.info("Stalled Data Line", 3,
"Audio playback data line has stopped accepting samples - recycling to clear the error");
mSourceDataLine.close();
openSourceDataLine();
}
}
//Check for completed and fully-played audio segment to closeout
if(mCurrentAudioSegment != null &&
mCurrentAudioSegment.isComplete() &&
(mCurrentBufferIndex >= mCurrentAudioSegment.getAudioBufferCount()))
{
disposeCurrentAudioSegment();
}
checkStop();
}
/**
@@ -555,156 +262,30 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
mCanProcessAudio = false;
if(mProcessorFuture != null)
{
mProcessorFuture.cancel(true);
}
mProcessorFuture = null;
disposeCurrentAudioSegment();
mAudioEventBroadcaster.clear();
mIdentifierCollectionListener = null;
if(mOutput != null)
if(mScheduledExecutorService != null)
{
mOutput.close();
mScheduledExecutorService.shutdownNow();
mScheduledExecutorService = null;
}
mOutput = null;
mAudioProvider.dispose();
mCanProcessAudio = false;
if(mSourceDataLine != null)
{
mSourceDataLine.stop();
mSourceDataLine.flush();
mSourceDataLine.close();
}
mSourceDataLine = null;
mGainControl = null;
mMuteControl = null;
}
/**
* Converts the audio buffer data into a byte buffer format appropriate for the underlying source data line.
*/
protected abstract ByteBuffer convert(float[] buffer);
/**
* Audio output channel name
*/
public String getChannelName()
{
return mMixerChannel.getLabel();
}
/**
* Mixer Channel for this audio output
*/
protected MixerChannel getMixerChannel()
{
return mMixerChannel;
}
/**
* Registers a single listener to receive audio start and audio stop events
*/
public void addAudioEventListener(Listener<AudioEvent> listener)
{
mAudioEventBroadcaster.addListener(listener);
}
public void removeAudioEventListener(Listener<AudioEvent> listener)
{
mAudioEventBroadcaster.removeListener(listener);
}
/**
* Broadcasts an audio event to the registered listener
*/
private void broadcastAudioEvent(AudioEvent audioEvent)
{
mAudioEventBroadcaster.broadcast(audioEvent);
}
/**
* Registers a single listener to receive the audio metadata from each
* audio packet
*/
public void setIdentifierCollectionListener(Listener<IdentifierCollection> listener)
{
mIdentifierCollectionListener = listener;
}
/**
* Unregisters the current audio metadata listener
*/
public void removeAudioMetadataListener()
{
mIdentifierCollectionListener = null;
}
/**
* Broadcasts audio identifier collection metadata to the registered listener
*/
private void broadcast(IdentifierCollection identifierCollection)
{
if(mIdentifierCollectionListener != null)
{
mIdentifierCollectionListener.receive(identifierCollection);
}
}
/**
* Starts audio playback once audio buffer is almost full and remaining capacity falls below the start threshold.
*
* Note: this method should only be invoked from the processAudio() method
*/
private void checkStart()
{
if(mCanProcessAudio && !mOutput.isRunning() && mOutput.available() <= mBufferStartThreshold)
{
mOutput.start();
mRunning = true;
}
}
/**
* Stops audio playback and drains the audio buffer to empty when the audio buffer is mostly empty and the
* available buffer capacity exceeds the stop threshold
*
* Note: this method should only be invoked from the processAudio() method
*/
private void checkStop()
{
if(mRunning)
{
//If the output buffer falls below the threshold then drain the output and stop playback
if(mOutput.isRunning())
{
if(mOutput.available() >= mBufferStopThreshold)
{
mOutput.drain();
mOutput.stop();
mRunning = false;
}
//Detect when we have a partially filled output that will neither start nor stop and the last write
//timestamp was more than the staleness threshold (500 ms) -- stop the output and discard stale contents
else if(mOutput.available() < mBufferStopThreshold &&
mOutput.available() > mBufferStartThreshold &&
(System.currentTimeMillis() - mOutputLastTimestamp >= STALE_PLAYBACK_THRESHOLD_MS))
{
mOutput.stop();
//Discard the stale buffer contents.
mOutput.flush();
mRunning = false;
}
}
//If output playback stopped on its own because the buffer emptied, then cleanup
else
{
mRunning = false;
}
//If we stopped audio playback, broadcast a null identifier to clear the gui panel
if(!mRunning)
{
broadcast(null);
}
}
mRunning = false;
}
/**
@@ -712,11 +293,13 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public void setMuted(boolean muted)
{
if(mMuteControl != null)
for(AudioChannel audioChannel: mAudioProvider.getAudioChannels())
{
mMuteControl.setValue(muted);
broadcastAudioEvent(new AudioEvent(muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED, getChannelName()));
audioChannel.setMuted(muted);
}
AudioEvent.Type type = muted ? AudioEvent.Type.AUDIO_MUTED : AudioEvent.Type.AUDIO_UNMUTED;
mAudioProvider.notify(type);
}
/**
@@ -724,9 +307,9 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
*/
public boolean isMuted()
{
if(mMuteControl != null)
if(!mAudioProvider.getAudioChannels().isEmpty())
{
return mMuteControl.getValue();
return mAudioProvider.getAudioChannels().getFirst().isMuted();
}
return false;
@@ -759,20 +342,20 @@ public abstract class AudioOutput implements LineListener, Listener<IdentifierUp
if(type == LineEvent.Type.START)
{
mAudioEventBroadcaster.broadcast(mAudioStartEvent);
mAudioProvider.notify(AudioEvent.Type.AUDIO_STARTED);
}
else if(type == LineEvent.Type.STOP)
{
mAudioEventBroadcaster.broadcast(mAudioStopEvent);
mAudioProvider.notify(AudioEvent.Type.AUDIO_STOPPED);
}
}
/**
* Runnable audio segment processor
* Audio Processor thread
*/
public class AudioSegmentProcessor implements Runnable
public class AudioProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private final AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,8 +20,6 @@ package io.github.dsheirer.audio.playback;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioException;
import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
@@ -29,81 +27,77 @@ import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import io.github.dsheirer.gui.theme.ThemedFontIcon;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.FloatControl;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* Audio playback panel
*/
public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
private static ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
private static ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
private IconModel mIconModel;
private SettingsManager mSettingsManager;
private IAudioController mController;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private JButton mMuteButton;
private static final ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
private static final ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
private final AliasModel mAliasModel;
private final AudioPlaybackManager mAudioPlaybackManager;
private final IconModel mIconModel;
private final SettingsManager mSettingsManager;
private final UserPreferences mUserPreferences;
private AudioChannelsPanel mAudioChannelsPanel;
private JButton mMuteButton;
/**
* Constructs an instance
* @param iconModel for icon lookup
* @param userPreferences for preference lookup
* @param settingsManager to monitor for changes
* @param audioPlaybackManager for accessing the audio output
* @param aliasModel for alias lookup
*/
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
IAudioController controller, AliasModel aliasModel)
AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel)
{
mIconModel = iconModel;
mSettingsManager = settingsManager;
mController = controller;
mAudioPlaybackManager = audioPlaybackManager;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mController.addControllerListener(this);
mAudioPlaybackManager.addAudioEventListener(this);
init();
}
/**
* Initialize the display
*/
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
setBackground(Color.BLACK);
mMuteButton = new MuteButton();
mMuteButton.setBackground(getBackground());
add(mMuteButton);
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
add(mAudioChannelsPanel);
addMouseListener(new MouseSelectionListener());
}
/**
* Receive audio event notifications from the audio playback controller
*/
@Override
public void receive(AudioEvent event)
{
@@ -112,19 +106,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
case AUDIO_CONFIGURATION_CHANGE_STARTED:
break;
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
remove(mAudioChannelsPanel);
mAudioChannelsPanel.dispose();
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
add(mAudioChannelsPanel);
mAudioChannelsPanel.repaint();
revalidate();
repaint();
}
EventQueue.invokeLater(() -> {
remove(mAudioChannelsPanel);
mAudioChannelsPanel.dispose();
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
add(mAudioChannelsPanel);
mAudioChannelsPanel.repaint();
revalidate();
repaint();
});
break;
default:
@@ -135,26 +124,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
/**
* Audio output mute control menu item.
*/
public class AudioOutputMuteItem extends JMenuItem
public static class AudioOutputMuteItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private AudioOutput mAudioOutput;
private final AudioOutput mAudioOutput;
/**
* Constructs an instance
* @param audioOutput to mute/unmute
*/
public AudioOutputMuteItem(AudioOutput audioOutput)
{
super(audioOutput.isMuted() ? "Unmute" : "Mute");
mAudioOutput = audioOutput;
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mAudioOutput.setMuted(!mAudioOutput.isMuted());
}
});
addActionListener(e -> mAudioOutput.setMuted(!mAudioOutput.isMuted()));
}
}
@@ -169,37 +151,23 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
if(SwingUtilities.isRightMouseButton(event))
{
JPopupMenu popup = new JPopupMenu();
/* Audio mixer/output selection menus */
JMenuItem outputMenu = new JMenuItem("Configure ...");
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
JMenuItem outputMenu = new JMenuItem("Audio Playback Device ...");
Icon icon = new ThemedFontIcon(FontAwesome.COG, 14);
outputMenu.setIcon(icon);
outputMenu.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
}
});
outputMenu.addActionListener(e -> MyEventBus.getGlobalEventBus()
.post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT)));
popup.add(outputMenu);
popup.add(new JPopupMenu.Separator());
/* Audio output mute and volume control */
for(AudioOutput output : mController.getAudioOutputs())
//Add optional gain controller for th eaudio output
if(mAudioPlaybackManager.getAudioOutput() != null && mAudioPlaybackManager.getAudioOutput().hasGainControl())
{
JMenu menu = new JMenu("Channel: " + output.getChannelName());
menu.add(new AudioOutputMuteItem(output));
if(output.hasGainControl())
{
JMenu volume = new JMenu("Volume");
volume.add(new VolumeSlider(output.getGainControl()));
menu.add(volume);
}
popup.add(menu);
popup.add(new JPopupMenu.Separator());
JMenuItem volume = new JMenuItem("Audio Volume");
volume.setEnabled(false);
Icon volumeIcon = new ThemedFontIcon(FontAwesome.VOLUME_UP, 14);
volume.setIcon(volumeIcon);
popup.add(volume);
popup.add(new VolumeSlider(mAudioPlaybackManager.getAudioOutput().getGainControl()));
}
popup.show(event.getComponent(), event.getX(), event.getY());
@@ -230,12 +198,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
/**
* Audio volume (gain) adjustment slider control
*/
public class VolumeSlider extends JSlider
public static class VolumeSlider extends JSlider
{
private static final long serialVersionUID = 1L;
private FloatControl mFloatControl;
private final FloatControl mFloatControl;
/**
* Constructs an instance
* @param control to be controlled
*/
public VolumeSlider(FloatControl control)
{
super(0, 100, 0);
@@ -244,21 +214,11 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
setMinorTickSpacing(5);
setPaintTicks(true);
setPaintLabels(true);
mFloatControl = control;
setValue(getIntegerValue(mFloatControl.getValue()));
addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent event)
{
mFloatControl.shift(mFloatControl.getValue(),
getFloatValue(VolumeSlider.this.getValue()),
1000);
}
});
addChangeListener(event -> mFloatControl.shift(mFloatControl.getValue(),
getFloatValue(VolumeSlider.this.getValue()),
1000));
addMouseListener(new MouseListener()
{
@@ -270,22 +230,10 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
VolumeSlider.this.setValue(50);
}
}
public void mouseReleased(MouseEvent arg0)
{
}
public void mousePressed(MouseEvent arg0)
{
}
public void mouseExited(MouseEvent arg0)
{
}
public void mouseEntered(MouseEvent arg0)
{
}
public void mouseReleased(MouseEvent arg0) {}
public void mousePressed(MouseEvent arg0) {}
public void mouseExited(MouseEvent arg0) {}
public void mouseEntered(MouseEvent arg0) {}
});
}
@@ -331,52 +279,12 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
}
}
/**
* Mixer/Channel configuration selection item
*/
public class MixerSelectionItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private MixerChannelConfiguration mConfiguration;
public MixerSelectionItem(MixerChannelConfiguration config)
{
super(config.toString());
mConfiguration = config;
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
try
{
mController.setMixerChannelConfiguration(mConfiguration);
}
catch(AudioException e1)
{
mLog.error("Couldn't set mixer channel configuration "
+ "to: " + mConfiguration.toString());
JOptionPane.showMessageDialog(MixerSelectionItem.this,
"Couldn't set [" + mConfiguration.toString() +
"] as the audio output device");
}
}
});
}
}
/**
* Mute button to mute all audio output channels exposed by the audio
* controller
*/
public class MuteButton extends JButton
{
private static final long serialVersionUID = 1L;
private boolean mMuted = false;
public MuteButton()
@@ -384,29 +292,13 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
setIcon(UNMUTED_ICON);
setBorderPainted(false);
getAccessibleContext().setAccessibleName("Mute");
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mMuted = !mMuted;
for(AudioOutput output : mController.getAudioOutputs())
{
output.setMuted(mMuted);
}
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
}
});
}
addActionListener(e -> {
mMuted = !mMuted;
mAudioPlaybackManager.getAudioOutput().setMuted(mMuted);
EventQueue.invokeLater(() -> {
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
});
});
}
}

View File

@@ -0,0 +1,123 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.playback;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Mixer;
/**
* Audio playback device descriptor for a mixer and a supported audio output format (ie number of channels).
*/
public class AudioPlaybackDeviceDescriptor implements Comparable<AudioPlaybackDeviceDescriptor>
{
private final Mixer.Info mMixerInfo;
private final AudioFormat mAudioFormat;
/**
* Constructs an instance
*
* @param mixerInfo for the mixer
* @param audioFormat supported by the mixer
*/
public AudioPlaybackDeviceDescriptor(Mixer.Info mixerInfo, AudioFormat audioFormat)
{
mMixerInfo = mixerInfo;
mAudioFormat = audioFormat;
}
@Override
public String toString()
{
return getCleanDescription(getMixerInfo()) + "(" + getMixerInfo().getName() + ") - " +
getChannelDescription(getAudioFormat());
}
/**
* Cleans the mixer description to reformat/reword certain mixer descriptions
* @param mixerInfo containing a description
* @return cleaned description
*/
private static String getCleanDescription(Mixer.Info mixerInfo)
{
if(mixerInfo.getDescription().contains("Direct Audio Device: "))
{
return mixerInfo.getDescription().replace("Direct Audio Device: ", "");
}
else if(mixerInfo.getDescription().contains("alsa_playback.java"))
{
return "Default (Java/ALSA)";
}
else
{
return mixerInfo.getDescription();
}
}
/**
* Provides a description of the channels for the audio format
* @param audioFormat with channels
* @return pretty string
*/
private static String getChannelDescription(AudioFormat audioFormat)
{
return switch(audioFormat.getChannels())
{
case 1 -> "Mono";
case 2 -> "Stereo";
case AudioSystem.NOT_SPECIFIED -> "Unknown number of channels";
default -> audioFormat.getChannels() + " Channels";
};
}
/**
* Audio format supported by the mixer
*
* @return audio format
*/
public AudioFormat getAudioFormat()
{
return mAudioFormat;
}
/**
* Mixer information for the available system mixer.
*/
public Mixer.Info getMixerInfo()
{
return mMixerInfo;
}
@Override
public int compareTo(AudioPlaybackDeviceDescriptor other)
{
if(other == null)
{
return 1;
}
if(other.getMixerInfo().getDescription().equals(getMixerInfo().getDescription()))
{
return Integer.compare(getAudioFormat().getChannels(), other.getAudioFormat().getChannels());
}
return getMixerInfo().getDescription().compareTo(other.getMixerInfo().getDescription());
}
}

View File

@@ -0,0 +1,171 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.playback;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.Line;
import javax.sound.sampled.Mixer;
/**
* Audio playback device manager.
*/
public class AudioPlaybackDeviceManager
{
/**
* Always returns the first available device in the system.
*/
public static AudioPlaybackDeviceDescriptor getDefaultAudioPLaybackDevice()
{
List<AudioPlaybackDeviceDescriptor> devices = getAudioPlaybackDevices();
if(!devices.isEmpty())
{
return devices.getFirst();
}
return null;
}
/**
* Finds the matching audio playback device
* @param name of the device
* @param channels count
* @return matching entry or the default playback device.
*/
public static AudioPlaybackDeviceDescriptor getAudioPlaybackDevice(String name, int channels)
{
for(AudioPlaybackDeviceDescriptor device: getAudioPlaybackDevices())
{
if(device.getMixerInfo().getName().equals(name) && device.getAudioFormat().getChannels() == channels)
{
return device;
}
}
return getDefaultAudioPLaybackDevice();
}
public static List<AudioPlaybackDeviceDescriptor> getAudioPlaybackDevices()
{
List<AudioPlaybackDeviceDescriptor> devices = new ArrayList<>();
Mixer.Info[] mixerInfos = AudioSystem.getMixerInfo();
for(Mixer.Info mixerInfo : mixerInfos)
{
Mixer mixer = AudioSystem.getMixer(mixerInfo);
if(mixer != null)
{
List<AudioFormat> discoveredFormats = new ArrayList<>();
for(Line.Info sourceLineInfo : mixer.getSourceLineInfo())
{
if(sourceLineInfo instanceof DataLine.Info dataLineInfo)
{
for(AudioFormat audioFormat : dataLineInfo.getFormats())
{
if(!audioFormat.isBigEndian() && audioFormat.getSampleSizeInBits() == 16 &&
!discoveredFormats.contains(audioFormat))
{
if(audioFormat.getChannels() == 1)
{
if(audioFormat.getSampleRate() == AudioSystem.NOT_SPECIFIED)
{
discoveredFormats.add(audioFormat);
//Modify the format for 8 kHz sample rate
AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
8000, 16, audioFormat.getChannels(), audioFormat.getFrameSize(),
8000, false);
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
}
else if(audioFormat.getSampleRate() == 8000)
{
discoveredFormats.add(audioFormat);
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, audioFormat));
}
}
//Stereo and 64-channel formats
else if(audioFormat.getChannels() == 2 || audioFormat.getChannels() > 8)
{
if(audioFormat.getSampleRate() == AudioSystem.NOT_SPECIFIED)
{
discoveredFormats.add(audioFormat);
//Modify the format for 8 kHz sample rate, 2 channels, 4 bytes per frame
AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
8000, 16, 2, 4,
8000, false);
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
}
else if(audioFormat.getSampleRate() == 8000)
{
discoveredFormats.add(audioFormat);
//Modify the format for 8 kHz sample rate, 2 channels, 4 bytes per frame
AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
audioFormat.getSampleRate(), 16, 2, 4,
audioFormat.getFrameRate(), false);
devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
}
}
//TODO: remove the 2 channels max restriction once supported
// else if(audioFormat.getChannels() > 2 && audioFormat.getChannels() <= 8)
// {
// if(audioFormat.getSampleRate() == AudioSystem.NOT_SPECIFIED)
// {
// discoveredFormats.add(audioFormat);
// //Modify the format for 8 kHz sample rate
// AudioFormat updatedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
// 8000, 16, audioFormat.getChannels(), audioFormat.getFrameSize(),
// 8000, false);
// devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, updatedFormat));
// }
// else if(audioFormat.getSampleRate() == 8000)
// {
// discoveredFormats.add(audioFormat);
// devices.add(new AudioPlaybackDeviceDescriptor(mixerInfo, audioFormat));
// }
// }
}
}
}
}
}
}
Collections.sort(devices);
return devices;
}
static void main()
{
List<AudioPlaybackDeviceDescriptor> devices = getAudioPlaybackDevices();
for(AudioPlaybackDeviceDescriptor device : devices)
{
System.out.println(device);
}
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -27,11 +27,11 @@ import io.github.dsheirer.controller.NamingThreadFactory;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.preference.PreferenceType;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.preference.playback.PlayTestAudioRequest;
import io.github.dsheirer.sample.Broadcaster;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.source.mixer.MixerChannel;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -51,23 +51,16 @@ import org.slf4j.LoggerFactory;
public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioController
{
private static final Logger mLog = LoggerFactory.getLogger(AudioPlaybackManager.class);
public static final AudioEvent CONFIGURATION_CHANGE_STARTED =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED, null);
public static final AudioEvent CONFIGURATION_CHANGE_COMPLETE =
new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE, null);
private Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private final AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
private final Broadcaster<AudioEvent> mControllerBroadcaster = new Broadcaster<>();
private final List<AudioSegment> mAudioSegments = new ArrayList<>();
private final List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
private final LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
private final ReentrantLock mAudioChannelsLock = new ReentrantLock();
private final UserPreferences mUserPreferences;
private AudioPlaybackDeviceDescriptor mAudioPlaybackDevice;
private AudioOutput mAudioOutput;
private ScheduledFuture<?> mProcessingTask;
private UserPreferences mUserPreferences;
private MixerChannelConfiguration mMixerChannelConfiguration;
private List<AudioOutput> mAudioOutputs = new ArrayList<>();
private List<AudioSegment> mAudioSegments = new ArrayList<>();
private List<AudioSegment> mPendingAudioSegments = new ArrayList<>();
private LinkedTransferQueue<AudioSegment> mNewAudioSegmentQueue = new LinkedTransferQueue<>();
private ScheduledExecutorService mScheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
private AudioSegmentPrioritySorter mAudioSegmentPrioritySorter = new AudioSegmentPrioritySorter();
private ReentrantLock mAudioOutputLock = new ReentrantLock();
/**
* Constructs an instance.
@@ -78,20 +71,18 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
mUserPreferences = userPreferences;
MyEventBus.getGlobalEventBus().register(this);
AudioPlaybackDeviceDescriptor device = mUserPreferences.getPlaybackPreference().getAudioPlaybackDevice();
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
if(configuration != null)
if(device != null)
{
try
{
setMixerChannelConfiguration(configuration);
setAudioPlaybackDevice(device);
}
catch(AudioException ae)
{
mLog.error("Error during setup of audio playback configuration. Attempted to use audio mixer [" +
(configuration != null ? configuration.getMixer().toString() : "null") + "] and channel [" +
(configuration != null ? configuration.getMixerChannel().name() : "null") + "]", ae);
mLog.error("Error during setup of audio playback configuration. Attempted to use device [" +
device + "]", ae);
}
}
else
@@ -99,13 +90,16 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
mLog.warn("No audio output devices available");
}
mProcessingTask = mScheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
//Even if we don't have an audio device, setup the queue processor to always process the audio segment queue
ScheduledExecutorService scheduledExecutorService =
Executors.newSingleThreadScheduledExecutor(new NamingThreadFactory("sdrtrunk audio manager"));
mProcessingTask = scheduledExecutorService.scheduleAtFixedRate(new AudioSegmentProcessor(),
0, 100, TimeUnit.MILLISECONDS);
}
/**
* Receives audio segments from channel audio modules.
* @param audioSegment
* @param audioSegment to receive and process
*/
@Override
public void receive(AudioSegment audioSegment)
@@ -179,6 +173,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
Iterator<AudioSegment> it = mAudioSegments.iterator();
AudioSegment audioSegment;
List<AudioSegment> displaced = new ArrayList<>();
//Remove any audio segments flagged as do not monitor. Don't remove completed segments yet, because
//we want to give them a brief chance at playback. Automatically assign linked audio segments to the
@@ -195,52 +190,67 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
else if(audioSegment.isLinked())
{
mAudioOutputLock.lock();
try
if(mAudioOutput != null)
{
for(AudioOutput audioOutput: mAudioOutputs)
mAudioChannelsLock.lock();
try
{
if(audioOutput.isLinkedTo(audioSegment))
for(AudioChannel audioOutput: mAudioOutput.getAudioProvider().getAudioChannels())
{
it.remove();
audioOutput.play(audioSegment);
if(audioOutput.isLinkedTo(audioSegment))
{
it.remove();
//Linked audio continues the current call, so it goes ahead of anything queued
AudioSegment queued = audioOutput.pollQueuedAudioSegment();
while(queued != null)
{
displaced.add(queued);
queued = audioOutput.pollQueuedAudioSegment();
}
audioOutput.play(audioSegment);
}
}
}
}
finally
{
mAudioOutputLock.unlock();
finally
{
mAudioChannelsLock.unlock();
}
}
}
}
mAudioSegments.addAll(displaced);
//Sort audio segments by playback priority and assign to empty audio outputs
if(!mAudioSegments.isEmpty())
if(!mAudioSegments.isEmpty() && mAudioOutput != null)
{
mAudioSegments.sort(mAudioSegmentPrioritySorter);
mAudioOutputLock.lock();
mAudioChannelsLock.lock();
try
{
//Assign empty audio outputs first
for(AudioOutput audioOutput: mAudioOutputs)
for(AudioChannel audioChannel: mAudioOutput.getAudioProvider().getAudioChannels())
{
if(audioOutput.isEmpty())
if(audioChannel.isEmpty())
{
audioOutput.play(mAudioSegments.remove(0));
audioChannel.play(mAudioSegments.removeFirst());
if(mAudioSegments.isEmpty())
{
return;
}
}
}
replaceLowerPriorityQueuedSegments(mAudioSegments, mAudioOutput.getAudioProvider().getAudioChannels());
}
finally
{
mAudioOutputLock.unlock();
mAudioChannelsLock.unlock();
}
}
@@ -260,6 +270,40 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
}
}
/**
* Swaps audio segments queued on the audio channels for waiting audio segments that have a higher playback priority,
* so that a segment queued behind the current playback can't hold a higher priority segment out of the channel.
* Audio that is currently playing is not interrupted. Displaced segments are returned to the waiting list.
*
* @param waiting audio segments, sorted by playback priority.
* @param audioChannels to evaluate.
*/
static void replaceLowerPriorityQueuedSegments(List<AudioSegment> waiting, List<AudioChannel> audioChannels)
{
for(AudioChannel audioChannel: audioChannels)
{
if(waiting.isEmpty())
{
return;
}
AudioSegment queued = audioChannel.peekQueuedAudioSegment();
if(queued != null &&
waiting.getFirst().monitorPriorityProperty().get() < queued.monitorPriorityProperty().get())
{
//The audio thread may have taken the queued segment since the peek, so only return what we removed.
queued = audioChannel.pollQueuedAudioSegment();
audioChannel.play(waiting.removeFirst());
if(queued != null)
{
waiting.add(queued);
}
}
}
}
public void dispose()
{
MyEventBus.getGlobalEventBus().unregister(this);
@@ -273,6 +317,19 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
mAudioSegments.clear();
}
/**
* Receives a request from the global event bus to playback a test audio sequence via the specified audio channel
* @param request with test audio and channel number
*/
@Subscribe
public void playTestAudio(PlayTestAudioRequest request)
{
if(mAudioOutput != null)
{
mAudioOutput.playTestAudio(request);
}
}
/**
* Receive user preference update notifications so that we can detect when the user changes the audio output
* device in the user preferences editor.
@@ -282,100 +339,112 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
{
if(preferenceType == PreferenceType.PLAYBACK)
{
MixerChannelConfiguration configuration = mUserPreferences.getPlaybackPreference().getMixerChannelConfiguration();
AudioPlaybackDeviceDescriptor device = mUserPreferences.getPlaybackPreference().getAudioPlaybackDevice();
if(configuration != null && !configuration.equals(mMixerChannelConfiguration))
if(device != null && !device.equals(getAudioPlaybackDevice()))
{
try
{
setMixerChannelConfiguration(configuration);
setAudioPlaybackDevice(device);
}
catch(AudioException ae)
{
mLog.error("Error changing audio output to [" + configuration + "]", ae);
mLog.error("Error changing audio output to [" + device + "]", ae);
}
}
}
}
/**
* Configures audio playback to use the configuration specified in the entry argument.
* Configures audio playback to use the audio device argument.
*
* @param entry to use in configuring the audio playback setup.
* @param audioDevice to use in configuring the audio playback setup.
* @throws AudioException if there is an error
*/
@Override
public void setMixerChannelConfiguration(MixerChannelConfiguration entry) throws AudioException
public void setAudioPlaybackDevice(AudioPlaybackDeviceDescriptor audioDevice) throws AudioException
{
if(entry != null)
if(audioDevice != null)
{
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_STARTED);
mControllerBroadcaster.broadcast(new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_STARTED,
audioDevice.getMixerInfo().getName()));
mAudioOutputLock.lock();
mAudioChannelsLock.lock();
try
{
for(AudioOutput audioOutput: mAudioOutputs)
if(mAudioOutput != null)
{
audioOutput.dispose();
mAudioOutput.dispose();
}
mAudioOutputs.clear();
int channelCount = audioDevice.getAudioFormat().getChannels();
switch(entry.getMixerChannel())
switch(channelCount)
{
case MONO:
AudioOutput mono = new MonoAudioOutput(entry.getMixer(), mUserPreferences);
mAudioOutputs.add(mono);
case 1:
mAudioOutput = new AudioOutput(audioDevice, new AudioProviderMono(mUserPreferences));
break;
case STEREO:
AudioOutput left = new StereoAudioOutput(entry.getMixer(), MixerChannel.LEFT, mUserPreferences);
mAudioOutputs.add(left);
AudioOutput right = new StereoAudioOutput(entry.getMixer(), MixerChannel.RIGHT, mUserPreferences);
mAudioOutputs.add(right);
case 2:
mAudioOutput = new AudioOutput(audioDevice, new AudioProviderStereo(mUserPreferences));
break;
default:
throw new AudioException("Unsupported mixer channel configuration: " + entry.getMixerChannel());
// mAudioOutput = new AudioOutput(audioDevice, new AudioProviderMultiChannel(mUserPreferences,
// audioDevice.getAudioFormat()));
throw new AudioException("Unsupported mixer channel configuration channel count: " + channelCount);
}
//Note: audio output can use an alternate device if the requested device can't be used, so we assign
//the descriptor that was actually used by the audio output
mAudioPlaybackDevice = mAudioOutput.getAudioPlaybackDeviceDescriptor();
}
finally
{
mAudioOutputLock.unlock();
mAudioChannelsLock.unlock();
}
mControllerBroadcaster.broadcast(CONFIGURATION_CHANGE_COMPLETE);
mMixerChannelConfiguration = entry;
mControllerBroadcaster.broadcast(new AudioEvent(AudioEvent.Type.AUDIO_CONFIGURATION_CHANGE_COMPLETE,
mAudioPlaybackDevice.getMixerInfo().getName()));
}
}
/**
* Audio output device. Note: this can be null depending on when it is accessed.
* @return audio output or null.
*/
public AudioOutput getAudioOutput()
{
return mAudioOutput;
}
/**
* Current audio playback mixer channel configuration setting.
*/
@Override
public MixerChannelConfiguration getMixerChannelConfiguration()
public AudioPlaybackDeviceDescriptor getAudioPlaybackDevice()
{
return mMixerChannelConfiguration;
return mAudioPlaybackDevice;
}
/**
* List of sorted audio outputs available for the current mixer channel configuration
*/
@Override
public List<AudioOutput> getAudioOutputs()
public List<AudioChannel> getAudioChannels()
{
List<AudioOutput> outputs = new ArrayList<>(mAudioOutputs);
if(mAudioOutput != null)
{
return mAudioOutput.getAudioProvider().getAudioChannels();
}
outputs.sort(Comparator.comparing(AudioOutput::getChannelName));
return outputs;
return Collections.emptyList();
}
/**
* Adds an audio event listener to receive audio event notifications.
*/
@Override
public void addControllerListener(Listener<AudioEvent> listener)
public void addAudioEventListener(Listener<AudioEvent> listener)
{
mControllerBroadcaster.addListener(listener);
}
@@ -384,7 +453,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
* Removes an audio event listener from receiving audio event notifications.
*/
@Override
public void removeControllerListener(Listener<AudioEvent> listener)
public void removeAudioEventListener(Listener<AudioEvent> listener)
{
mControllerBroadcaster.removeListener(listener);
}
@@ -394,7 +463,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
*/
public class AudioSegmentProcessor implements Runnable
{
private AtomicBoolean mProcessing = new AtomicBoolean();
private final AtomicBoolean mProcessing = new AtomicBoolean();
@Override
public void run()
@@ -418,7 +487,7 @@ public class AudioPlaybackManager implements Listener<AudioSegment>, IAudioContr
/**
* Audio segment comparator for sorting audio segments by: 1)Playback priority and 2)Segment start time
*/
public class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
public static class AudioSegmentPrioritySorter implements Comparator<AudioSegment>
{
@Override
public int compare(AudioSegment segment1, AudioSegment segment2)

View File

@@ -0,0 +1,82 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.playback;
import io.github.dsheirer.audio.AudioEvent;
import java.nio.ByteBuffer;
import java.util.List;
import javax.sound.sampled.AudioFormat;
/**
* Abstract audio provider implementation
*/
public abstract class AudioProvider
{
/**
* Converts the audio sample to a 16-bit PCM sample, limiting out of range values instead of allowing them to wrap.
* @param sample in range -1.0 to 1.0
* @return PCM sample
*/
protected static short toPcm(float sample)
{
return (short)(Math.clamp(sample, -1.0f, 1.0f) * Short.MAX_VALUE);
}
/**
* Audio format for this provider
*/
public abstract AudioFormat getAudioFormat();
/**
* Provide a non-null, 160-sample/20-millisecond buffer of audio at 8 kHz
* @return audio buffer
*/
public abstract ByteBuffer getAudio();
/**
* Provides access to the audio channels for this provider
* @return list of audio channels
*/
public abstract List<AudioChannel> getAudioChannels();
/**
* Notifies audio event listener(s) of the event
* @param eventType to broadcast
*/
public void notify(AudioEvent.Type eventType)
{
for(AudioChannel audioChannel : getAudioChannels())
{
audioChannel.notify(eventType);
}
}
/**
* Disposes the audio channel to prepare for audio output changes or shutdown.
*/
public void dispose()
{
for(AudioChannel audioChannel: getAudioChannels())
{
audioChannel.dispose();
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.List;
import javax.sound.sampled.AudioFormat;
/**
* Single channel (mono) audio provider wraps a single audio channel and provides converted audio.
*/
public class AudioProviderMono extends AudioProvider
{
private static final ByteBuffer SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2);
private final AudioChannel mAudioChannel;
/**
* Constructs an instance
* @param userPreferences for monitoring preference changes
*/
public AudioProviderMono(UserPreferences userPreferences)
{
mAudioChannel = new AudioChannel(userPreferences, "MONO");
}
@Override
public List<AudioChannel> getAudioChannels()
{
return List.of(mAudioChannel);
}
@Override
public AudioFormat getAudioFormat()
{
return AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO;
}
@Override
public ByteBuffer getAudio()
{
float[] audio = mAudioChannel.getAudio();
if(audio == null)
{
if(!mAudioChannel.hasAudioSegment())
{
mAudioChannel.clearMetadata();
}
return SILENCE;
}
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
for(int x = 0; x < AudioChannel.SAMPLES_PER_INTERVAL; x++)
{
shortBuffer.put(toPcm(audio[x]));
}
return buffer;
}
}

View File

@@ -0,0 +1,123 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.playback;
import io.github.dsheirer.preference.UserPreferences;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.sound.sampled.AudioFormat;
/**
* Multi-Channel audio provider implementation. Provides an interleaved stream of audio from multiple channels.
*
* Note: this implementation has not been tested for use.
*/
public class AudioProviderMultiChannel extends AudioProvider
{
private static ByteBuffer SILENCE;
private final List<AudioChannel> mAudioChannels = new ArrayList<>();
private final AudioFormat mAudioFormat;
/**
* Constructs a multi-channel audio provider
* @param userPreferences for this provider
* @param audioFormat for this provider
*/
public AudioProviderMultiChannel(UserPreferences userPreferences, AudioFormat audioFormat)
{
SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * audioFormat.getChannels());
for(int channel = 0; channel < audioFormat.getChannels(); channel++)
{
mAudioChannels.add(new AudioChannel(userPreferences, String.valueOf(channel)));
}
mAudioFormat = audioFormat;
}
@Override
public AudioFormat getAudioFormat()
{
return mAudioFormat;
}
/**
* Access the next buffer of audio.
* @return next buffer or null if there is no audio available
*/
@Override
public ByteBuffer getAudio()
{
float[][] samples = new float[mAudioChannels.size()][];
boolean hasAudio = false;
AudioChannel channel = null;
for(int x = 0; x < mAudioChannels.size(); x++)
{
channel = mAudioChannels.get(x);
samples[x] = channel.getAudio();
if(samples[x] == null)
{
if(!channel.hasAudioSegment())
{
channel.clearMetadata();
}
samples[x] = new float[AudioChannel.SAMPLES_PER_INTERVAL];
}
else
{
hasAudio = true;
}
}
if(!hasAudio)
{
return SILENCE;
}
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 2 *
mAudioChannels.size()).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
for(int sampleCounter = 0; sampleCounter < AudioChannel.SAMPLES_PER_INTERVAL; sampleCounter++)
{
for(int channelNumber = 0; channelNumber < mAudioChannels.size(); channelNumber++)
{
shortBuffer.put(toPcm(samples[channelNumber][sampleCounter]));
}
}
return buffer;
}
@Override
public List<AudioChannel> getAudioChannels()
{
return Collections.unmodifiableList(mAudioChannels);
}
}

View File

@@ -0,0 +1,111 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2026 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.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.List;
import javax.sound.sampled.AudioFormat;
/**
* Provides an interleaved stream of left and right audio samples from audio channels.
*
* Note: the getAudio() method is optimized to provide buffers of 160 samples (x2) of audio per interval, equivalent to
* 20 milliseconds of audio at 8,000 Hertz sample rate.
*/
public class AudioProviderStereo extends AudioProvider
{
private final AudioChannel mAudioChannelLeft;
private final AudioChannel mAudioChannelRight;
private static final ByteBuffer SILENCE = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 4);
/**
* Constructs an instance.
* @param userPreferences for monitoring preference changes
*/
public AudioProviderStereo(UserPreferences userPreferences)
{
mAudioChannelLeft = new AudioChannel(userPreferences, "LEFT");
mAudioChannelRight = new AudioChannel(userPreferences, "RIGHT");
}
@Override
public AudioFormat getAudioFormat()
{
return AudioFormats.PCM_SIGNED_8000_HZ_16BITS_STEREO;
}
@Override
public List<AudioChannel> getAudioChannels()
{
return List.of(mAudioChannelLeft, mAudioChannelRight);
}
/**
* Access the next buffer of audio.
* @return next buffer or null if there is no audio available
*/
@Override
public ByteBuffer getAudio()
{
float[] left = mAudioChannelLeft.getAudio();
if(left == null && !mAudioChannelLeft.hasAudioSegment())
{
mAudioChannelLeft.clearMetadata();
}
float[] right = mAudioChannelRight.getAudio();
if(right == null && !mAudioChannelRight.hasAudioSegment())
{
mAudioChannelRight.clearMetadata();
}
//If we didn't get audio from either channel send silence buffer
if(left == null && right == null)
{
return SILENCE;
}
else if(left == null)
{
left = new float[AudioChannel.SAMPLES_PER_INTERVAL];
}
else if(right == null)
{
right = new float[AudioChannel.SAMPLES_PER_INTERVAL];
}
ByteBuffer buffer = ByteBuffer.allocate(AudioChannel.SAMPLES_PER_INTERVAL * 4).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
for(int x = 0; x < AudioChannel.SAMPLES_PER_INTERVAL; x++)
{
shortBuffer.put(toPcm(left[x]));
shortBuffer.put(toPcm(right[x]));
}
return buffer;
}
}

View File

@@ -1,65 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
/**
* Mono Audio output implementation
*/
public class MonoAudioOutput extends AudioOutput
{
private final static int BUFFER_SIZE = 8000;
public MonoAudioOutput(Mixer mixer, UserPreferences userPreferences)
{
super(mixer, MixerChannel.MONO, AudioFormats.PCM_SIGNED_8000_HZ_16_BIT_MONO,
AudioFormats.MONO_SOURCE_DATALINE_INFO, BUFFER_SIZE, userPreferences);
}
/**
* Converts the audio packet data into mono audio frames.
*/
protected ByteBuffer convert(float[] samples)
{
ByteBuffer buffer = null;
if(samples.length > 0)
{
/* Little-endian byte buffer */
buffer = ByteBuffer.allocate(samples.length * 2).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
for(float sample : samples)
{
shortBuffer.put((short) (sample * Short.MAX_VALUE));
}
}
return buffer;
}
}

View File

@@ -1,79 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.audio.playback;
import io.github.dsheirer.audio.AudioFormats;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.source.mixer.MixerChannel;
import javax.sound.sampled.Mixer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
/**
* Stereo audio output implementation.
*/
public class StereoAudioOutput extends AudioOutput
{
private final static int BUFFER_SIZE = 16000;
public StereoAudioOutput(Mixer mixer, MixerChannel channel, UserPreferences userPreferences)
{
super(mixer, channel, AudioFormats.PCM_SIGNED_8000_HZ_16BITS_STEREO, AudioFormats.STEREO_SOURCE_DATALINE_INFO,
BUFFER_SIZE, userPreferences);
}
/**
* Converts the audio packet data into stereo audio frames with the mixer
* channel containing the audio and the other channel containing zero
* valued (silent) samples.
*/
protected ByteBuffer convert(float[] samples)
{
ByteBuffer buffer = null;
if(samples.length > 0)
{
/* Little-endian byte buffer */
buffer = ByteBuffer.allocate(samples.length * 4).order(ByteOrder.LITTLE_ENDIAN);
ShortBuffer shortBuffer = buffer.asShortBuffer();
if(getMixerChannel() == MixerChannel.LEFT)
{
for(float sample : samples)
{
shortBuffer.put((short) (sample * Short.MAX_VALUE));
shortBuffer.put((short) 0);
}
}
else
{
for(float sample : samples)
{
shortBuffer.put((short) 0);
shortBuffer.put((short) (sample * Short.MAX_VALUE));
}
}
}
return buffer;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -243,6 +243,16 @@ public class BinaryMessage extends BitSet
return mSize;
}
/**
* Current size of the message indicating the quantity of bits that have been added to this message. This value
* is less than or equal to the size() value.
* @return current size in bits.
*/
public int currentSize()
{
return mPointer;
}
public void setSize(int size)
{
mSize = size;
@@ -281,7 +291,30 @@ public class BinaryMessage extends BitSet
}
}
/**
* Adds bit1 and bit2 to this bitset.
*
* Note: this method does not provide overfill protection.
*
* @param bit1 to add.
* @param bit2 to add.
*/
public void add(boolean bit1, boolean bit2)
{
set(mPointer++, bit1);
set(mPointer++, bit2);
}
public String toString()
{
return toHexString();
}
/**
* Binary representation of the message
* @return message in binary 0/1 format
*/
public String toBinaryString()
{
StringBuilder sb = new StringBuilder();
@@ -293,6 +326,10 @@ public class BinaryMessage extends BitSet
return sb.toString();
}
/**
* Hex string representation of the message
* @return message in hex
*/
public String toHexString()
{
StringBuilder sb = new StringBuilder();
@@ -401,8 +438,8 @@ public class BinaryMessage extends BitSet
boolean[] returnValue = null;
if(startIndex >= 0 &&
startIndex < endIndex &&
endIndex < mSize)
startIndex < endIndex &&
endIndex < mSize)
{
returnValue = new boolean[endIndex - startIndex + 1];
@@ -433,7 +470,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -662,7 +699,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -697,6 +734,28 @@ public class BinaryMessage extends BitSet
}
}
/**
* Sets the integer value to the field described by the argument.
* @param value to set
* @param intField describing the indices of the field.
*/
public void setInt(int value, IntField intField)
{
for(int x = 0; x < intField.width(); x++)
{
int mask = 1 << (intField.width() - x - 1);
if((value & mask) == mask)
{
set(intField.start() + x);
}
else
{
clear(intField.start() + x);
}
}
}
/**
* Returns the byte value represented by the bit array
*
@@ -710,7 +769,7 @@ public class BinaryMessage extends BitSet
if(bits.length != 8)
{
throw new IllegalArgumentException("Invalid - there must be 8"
+ "indexes to form a proper byte");
+ "indexes to form a proper byte");
}
int value = 0;
@@ -742,7 +801,7 @@ public class BinaryMessage extends BitSet
if(bits.length != 8)
{
throw new IllegalArgumentException("Invalid - there must be 8"
+ "indexes to form a proper byte");
+ "indexes to form a proper byte");
}
int value = 0;
@@ -870,7 +929,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -902,7 +961,7 @@ public class BinaryMessage extends BitSet
if(bits.length > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -951,6 +1010,18 @@ public class BinaryMessage extends BitSet
return String.format("%0" + width + "X", getInt(field));
}
/**
* Returns the long field formatted as a hex value using zero prefixes to pad the hex character count to fully
* represent the size (width) of the field.
* @param field to parse as hex
* @return hex value.
*/
public String getHex(LongField field)
{
int width = Math.ceilDiv(field.width(), 4);
return String.format("%0" + width + "X", getLong(field));
}
/**
* Format the byte value that starts at the specified index as hexadecimal. If the length of the message is less
* than the start index plus 7 bits, then the value represents those bits as high-order bits with zero padding in
@@ -990,7 +1061,7 @@ public class BinaryMessage extends BitSet
else
{
throw new IllegalArgumentException("BitSetBuffer.getHex() "
+ "maximum array length is 63 bits");
+ "maximum array length is 63 bits");
}
}
@@ -1011,7 +1082,7 @@ public class BinaryMessage extends BitSet
if(FastMath.abs(end - start) > 32)
{
throw new IllegalArgumentException("Overflow - must be 32 bits "
+ "or less to fit into a primitive integer value");
+ "or less to fit into a primitive integer value");
}
int value = 0;
@@ -1086,7 +1157,7 @@ public class BinaryMessage extends BitSet
if(FastMath.abs(end - start) > 64)
{
throw new IllegalArgumentException("Overflow - must be 64 bits "
+ "or less to fit into a primitive long value");
+ "or less to fit into a primitive long value");
}
long value = 0;
@@ -1241,7 +1312,7 @@ public class BinaryMessage extends BitSet
if(!message.matches("[01]*"))
{
throw new IllegalArgumentException(
"Message must contain only zeros and ones");
"Message must contain only zeros and ones");
}
BinaryMessage buffer = new BinaryMessage(message.length());
@@ -1269,37 +1340,15 @@ public class BinaryMessage extends BitSet
throw new IllegalArgumentException("Message must contain only 0-9 and A-F hexadecimal characters");
}
int length = hex.length() / 2;
BinaryMessage message = new BinaryMessage(hex.length() * 4);
if(length * 2 < hex.length())
for(int x = 0; x < hex.length(); x++)
{
length++;
String rawHex = hex.substring(x, x + 1);
message.setInt(Integer.parseInt(rawHex, 16), IntField.length4(x * 4));
}
BinaryMessage buffer = new BinaryMessage(length * 8);
int bufferOffset = 0;
for(int x = 0; x < hex.length(); x += 2)
{
int endIndex = x + 2;
if(endIndex > hex.length())
{
endIndex--;
}
String rawHex = hex.substring(x, endIndex);
if(rawHex.length() == 1)
{
rawHex = rawHex + "0";
}
int value = Integer.parseInt(rawHex, 16);
buffer.setByte(bufferOffset, (byte)(value & 0xFF));
bufferOffset += 8;
}
return buffer;
return message;
}
/**

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,6 +18,7 @@
*/
package io.github.dsheirer.bits;
import java.nio.ByteOrder;
import java.util.BitSet;
public class CorrectedBinaryMessage extends BinaryMessage
@@ -54,6 +55,45 @@ public class CorrectedBinaryMessage extends BinaryMessage
this.xor(message);
}
/**
* Creates a new message loaded from the hexadecimal string
* @param hex representing the bits of the message
* @return new message
*/
public static CorrectedBinaryMessage loadHex(String hex)
{
return new CorrectedBinaryMessage(BinaryMessage.loadHex(hex));
}
/**
* Overloads the parent method to return this type.
* @param binary message to load
* @return loaded message
*/
public static CorrectedBinaryMessage load(String binary)
{
return new CorrectedBinaryMessage(BinaryMessage.load(binary));
}
/**
* Constructs a binary frame from the byte array with length set to the
* number of bits contained in the byte array
*/
public static CorrectedBinaryMessage from(byte[] data, ByteOrder byteOrder)
{
if(byteOrder == ByteOrder.LITTLE_ENDIAN)
{
return new CorrectedBinaryMessage(from(data));
}
else
{
CorrectedBinaryMessage frame = new CorrectedBinaryMessage(data.length * 8);
BitSet bitSet = BitSet.valueOf(data);
frame.xor(bitSet);
return frame;
}
}
@Override
public int getCorrectedBitCount()
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -60,7 +60,27 @@ public record IntField(int start, int end)
}
/**
* Utility constructor method for a field with one four bits of length.
* Utility constructor method for a field with two bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length2(int start)
{
return new IntField(start, (start + 1));
}
/**
* Utility constructor method for a field with three bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length3(int start)
{
return new IntField(start, (start + 2));
}
/**
* Utility constructor method for a field with four bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
@@ -69,6 +89,36 @@ public record IntField(int start, int end)
return new IntField(start, (start + 3));
}
/**
* Utility constructor method for a field with five bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length5(int start)
{
return new IntField(start, (start + 4));
}
/**
* Utility constructor method for a field with six bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length6(int start)
{
return new IntField(start, (start + 5));
}
/**
* Utility constructor method for a field with seven bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length7(int start)
{
return new IntField(start, (start + 6));
}
/**
* Utility constructor method for a field with one octet of length.
* @param start index (inclusive)
@@ -79,6 +129,36 @@ public record IntField(int start, int end)
return new IntField(start, (start + 7));
}
/**
* Utility constructor method for a field with nine bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length9(int start)
{
return new IntField(start, (start + 8));
}
/**
* Utility constructor method for a field with ten bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length10(int start)
{
return new IntField(start, (start + 9));
}
/**
* Utility constructor method for a field with eleven bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length11(int start)
{
return new IntField(start, (start + 10));
}
/**
* Utility constructor method for a field with 12 bits of length.
* @param start index (inclusive)
@@ -89,6 +169,26 @@ public record IntField(int start, int end)
return new IntField(start, (start + 11));
}
/**
* Utility constructor method for a field with 14 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length14(int start)
{
return new IntField(start, (start + 13));
}
/**
* Utility constructor method for a field with 15 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static IntField length15(int start)
{
return new IntField(start, (start + 14));
}
/**
* Utility constructor method for a field with two octets of length.
* @param start index (inclusive)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2024 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -39,6 +39,15 @@ public record LongField(int start, int end)
}
}
/**
* Width of the field in bit positions
* @return width
*/
public int width()
{
return end() - start() + 1;
}
/**
* Utility constructor method.
* @param start index (inclusive)
@@ -101,7 +110,17 @@ public record LongField(int start, int end)
}
/**
* Utility constructor method for a field with two octets of length.
* Utility constructor method for a field with 23 bits of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length23(int start)
{
return new LongField(start, (start + 22));
}
/**
* Utility constructor method for a field with three octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
@@ -111,7 +130,7 @@ public record LongField(int start, int end)
}
/**
* Utility constructor method for a field with three octets of length.
* Utility constructor method for a field with four octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
@@ -119,4 +138,24 @@ public record LongField(int start, int end)
{
return new LongField(start, (start + 31));
}
/**
* Utility constructor method for a field with seven octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length56(int start)
{
return new LongField(start, (start + 55));
}
/**
* Utility constructor method for a field with eight octets of length.
* @param start index (inclusive)
* @return constructed bit field.
*/
public static LongField length64(int start)
{
return new LongField(start, (start + 63));
}
}

View File

@@ -1,35 +1,31 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2025 Dennis Sheirer
*
* * ******************************************************************************
* * Copyright (C) 2014-2020 Dennis Sheirer
* *
* * This program is free software: you can redistribute it and/or modify
* * it under the terms of the GNU General Public License as published by
* * the Free Software Foundation, either version 3 of the License, or
* * (at your option) any later version.
* *
* * This program is distributed in the hope that it will be useful,
* * but WITHOUT ANY WARRANTY; without even the implied warranty of
* * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* * GNU General Public License for more details.
* *
* * You should have received a copy of the GNU General Public License
* * along with this program. If not, see <http://www.gnu.org/licenses/>
* * *****************************************************************************
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.Validate;
import org.apache.commons.math3.util.FastMath;
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
* comparision against the incoming bit stream.
@@ -39,6 +35,7 @@ import java.util.List;
* of the Long.rotate methods will wrap the MSB around to the LSB and
* corrupt the matching value.
*/
@Deprecated //remove after P25P2 decoder is updated
public class MultiSyncPatternMatcher
{
private final static Logger mLog = LoggerFactory.getLogger(MultiSyncPatternMatcher.class);

View File

@@ -1,23 +1,27 @@
/*******************************************************************************
* sdr-trunk
* Copyright (C) 2014-2018 Dennis Sheirer
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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.bits;
import io.github.dsheirer.dsp.symbol.ISyncDetectListener;
import org.apache.commons.lang3.Validate;
@Deprecated //Remove after P25P2 decoder is updated.
public class SoftSyncDetector implements ISyncProcessor
{
private ISyncDetectListener mListener;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,7 +29,7 @@ import java.util.Iterator;
*/
public class ByteNativeBuffer extends AbstractNativeBuffer
{
private static final int FRAGMENT_SIZE = 2048;
private static final int FRAGMENT_SIZE = 8192;
private final static float[] LOOKUP_VALUES;
private float mAverageDc;

View File

@@ -1,57 +1,153 @@
/*******************************************************************************
* 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/>
******************************************************************************/
/*
* *****************************************************************************
* Copyright (C) 2014-2025 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;
/**
* Averaging buffer backed by a ring buffer implemented with a float array.
*/
public class FloatAveragingBuffer
{
private float[] mBuffer;
private float mAverage = 0.0f;
private int mBufferSize;
private int mBufferPointer;
public FloatAveragingBuffer( int size )
{
mBufferSize = size;
mBuffer = new float[ size ];
}
public float get( float newValue )
{
float oldValue = mBuffer[ mBufferPointer ];
private final float[] mBuffer;
private final int mBufferSize;
private float mAverage = 0.0f;
private int mBufferPointer;
private boolean mRapidFill = false;
private int mRapidFillCounter = 0;
private int mRapidFillIncrement;
if( Float.isInfinite( newValue ) || Float.isNaN( newValue ) )
{
mAverage = mAverage - ( oldValue / mBufferSize );
/**
* Constructs an instance that uses a rapid fill increment value where the initial values get counted multiple times
* to more quickly get to the average value instead of walking up to the average from the default average value of 0.
* @param size of buffer
* @param rapidFillIncrement indicating how many value additions per individual add to execute until the averaging
* buffer is filled. After the initial fill state is achieved, reverts to using a single value addition.
*/
public FloatAveragingBuffer(int size, int rapidFillIncrement)
{
this(size);
mRapidFillIncrement = rapidFillIncrement;
mRapidFill = true;
}
mBuffer[ mBufferPointer++ ] = 0.0f;
}
else
{
mAverage = mAverage - ( oldValue / mBufferSize ) + ( newValue / mBufferSize );
/**
* Constructs an instance that uses a ring buffer of the specified size.
* @param size of the averaging buffer.
*/
public FloatAveragingBuffer(int size)
{
mBufferSize = size;
mBuffer = new float[size];
}
mBuffer[ mBufferPointer++ ] = newValue;
}
/**
* Resets this buffer to an empty state
*/
public void reset()
{
Arrays.fill(mBuffer, 0);
mBufferPointer = 0;
mAverage = 0.0f;
if( mBufferPointer >= mBufferSize )
{
mBufferPointer = 0;
}
if(mRapidFillIncrement > 0)
{
mRapidFill = true;
mRapidFillCounter = 0;
}
}
return mAverage;
}
/**
* Current average value.
* @return average
*/
public float getAverage()
{
return mAverage;
}
/**
* Adds the value to the buffer and updates the average.
* @param value to add
*/
public void add(float value)
{
if(mRapidFill)
{
for(int x = 0; x < mRapidFillIncrement; x++)
{
float oldValue = mBuffer[mBufferPointer];
if(Float.isInfinite(value) || Float.isNaN(value))
{
mAverage = mAverage - (oldValue / mBufferSize);
mBuffer[mBufferPointer++] = 0.0f;
}
else
{
mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize);
mBuffer[mBufferPointer++] = value;
}
if(mBufferPointer >= mBufferSize)
{
mBufferPointer = 0;
}
}
mRapidFillCounter += mRapidFillIncrement;
if(mRapidFillCounter > mBufferSize)
{
mRapidFill = false;
}
}
else
{
float oldValue = mBuffer[mBufferPointer];
if(Float.isInfinite(value) || Float.isNaN(value))
{
mAverage = mAverage - (oldValue / mBufferSize);
mBuffer[mBufferPointer++] = 0.0f;
}
else
{
mAverage = mAverage - (oldValue / mBufferSize) + (value / mBufferSize);
mBuffer[mBufferPointer++] = value;
}
if(mBufferPointer >= mBufferSize)
{
mBufferPointer = 0;
}
}
}
/**
* Adds the value to the buffer and calculates a new average.
* @param value to add
* @return updated average
*/
public float get(float value)
{
add(value);
return mAverage;
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2026 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
@@ -98,6 +98,33 @@ public class FloatCircularBuffer
return values;
}
/**
* Gets all sample values in reverse order.
*/
public float[] getAllReversed()
{
int valuePointer = 0;
float[] values = new float[mBuffer.length];
int bufferPointer = mBufferPointer - 1;
if(bufferPointer < 0)
{
bufferPointer = mBuffer.length - 1;
}
while(valuePointer < values.length)
{
values[valuePointer++] = mBuffer[bufferPointer--];
if(bufferPointer < 0)
{
bufferPointer = mBuffer.length - 1;
}
}
return values;
}
/**
* Places the new value into the buffer, overwriting the oldest value
* @param newValue to add to the buffer

View File

@@ -100,8 +100,10 @@ public class FloatNativeBuffer extends AbstractNativeBuffer
throw new IllegalStateException("No more samples");
}
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
mBufferPointer += BUFFER_SIZE * 2;
//The final chunk holds the remaining samples, so that it isn't padded with zero valued samples
int end = Math.min(mBufferPointer + BUFFER_SIZE * 2, mInterleavedComplexSamples.length);
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, end);
mBufferPointer = end;
return SampleUtils.deinterleave(chunk, getTimestamp());
}
@@ -125,8 +127,10 @@ public class FloatNativeBuffer extends AbstractNativeBuffer
throw new IllegalStateException("No more samples");
}
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, mBufferPointer + BUFFER_SIZE * 2);
mBufferPointer += BUFFER_SIZE * 2;
//The final chunk holds the remaining samples, so that it isn't padded with zero valued samples
int end = Math.min(mBufferPointer + BUFFER_SIZE * 2, mInterleavedComplexSamples.length);
float[] chunk = Arrays.copyOfRange(mInterleavedComplexSamples, mBufferPointer, end);
mBufferPointer = end;
return new InterleavedComplexSamples(chunk, getTimestamp());
}

View File

@@ -116,7 +116,8 @@ public class NativeSampleDelayBuffer implements Listener<INativeBuffer>
actionRequest = mActionQueue.poll();
}
mBroadcaster.receive(samples);
//The delay queue stores the unconverted buffer since it's more compact than the converted samples
mBroadcaster.receive(mBroadcaster.getListenerCount() > 1 ? new SharedNativeBuffer(samples) : samples);
//Store the new buffer in the delay queue and increment the pointer
mDelayBuffer[mDelayBufferPointer++] = samples;

View File

@@ -0,0 +1,114 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
package io.github.dsheirer.buffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
/**
* Native buffer wrapper that lets multiple channels share one conversion of the native samples. Each channel's
* iterator returns the same complex samples instances, on the channel's own thread, so consumers must not modify the
* samples. The converted samples are released when the channels release this wrapper.
*/
public class SharedNativeBuffer implements INativeBuffer
{
private final INativeBuffer mNativeBuffer;
private final List<ComplexSamples> mConverted = new ArrayList<>();
private Iterator<ComplexSamples> mConverter;
/**
* Constructs an instance
* @param nativeBuffer to convert
*/
public SharedNativeBuffer(INativeBuffer nativeBuffer)
{
mNativeBuffer = nativeBuffer;
}
/**
* Indicates if the converted samples include the index, converting the next samples as needed.
*/
private synchronized boolean hasConverted(int index)
{
if(mConverter == null)
{
mConverter = mNativeBuffer.iterator();
}
if(index == mConverted.size() && mConverter.hasNext())
{
mConverted.add(mConverter.next());
}
return index < mConverted.size();
}
private synchronized ComplexSamples getConverted(int index)
{
return mConverted.get(index);
}
@Override
public Iterator<ComplexSamples> iterator()
{
return new Iterator<>()
{
private int mIndex = 0;
@Override
public boolean hasNext()
{
return hasConverted(mIndex);
}
@Override
public ComplexSamples next()
{
if(!hasNext())
{
throw new NoSuchElementException("No more samples");
}
return getConverted(mIndex++);
}
};
}
@Override
public Iterator<InterleavedComplexSamples> iteratorInterleaved()
{
return mNativeBuffer.iteratorInterleaved();
}
@Override
public int sampleCount()
{
return mNativeBuffer.sampleCount();
}
@Override
public long getTimestamp()
{
return mNativeBuffer.getTimestamp();
}
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy.hf;
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy.hf;
package io.github.dsheirer.buffer.airspy;
import io.github.dsheirer.buffer.AbstractNativeBufferFactory;
import io.github.dsheirer.buffer.DcCorrectionManager;

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,18 +17,18 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import java.nio.ByteBuffer;
/**
* Interface to convert airspy samples from a byte buffer to an array of shorts.
* Interface to convert samples from a byte buffer to an array of shorts.
*/
public interface IAirspySampleConverter
public interface ISampleConverter
{
/**
* Converts the airspy byte samples contained in the byte buffer to their short-valued representation
* @param buffer of airspy samples
* Converts the byte samples contained in the byte buffer to their short-valued representation
* @param buffer of samples
* @return converted samples
*/
short[] convert(ByteBuffer buffer);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,20 +17,20 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.dsp.filter.hilbert.HilbertTransform;
import java.util.Iterator;
/**
* Base complex samples iterator for raw Airspy sample buffers. Incorporates a Hilbert transform filter
* Base complex samples iterator for raw sample buffers. Incorporates a Hilbert transform filter
* for converting the real sample array to complex samples.
* @param <T> either ComplexSamples or InterleavedComplexSamples
*/
public abstract class AirspyBufferIterator<T> implements Iterator<T>
public abstract class SampleBufferIterator<T> implements Iterator<T>
{
protected static final float SCALE_SIGNED_12_BIT_TO_FLOAT = 1.0f / 2048.0f;
protected static final int FRAGMENT_SIZE = 2048;
protected static final int FRAGMENT_SIZE = 4096;
protected static final float[] COEFFICIENTS =
HilbertTransform.convertHalfBandToHilbert(HilbertTransform.HALF_BAND_FILTER_47_TAP);
@@ -47,14 +47,14 @@ public abstract class AirspyBufferIterator<T> implements Iterator<T>
/**
* Constructs an instance
* @param samples from the airspy.
* @param samples from the tuner.
* @param residualI samples from previous buffer
* @param residualQ samples from previous buffer
* @param averageDc as measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
public SampleBufferIterator(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp,
float samplesPerMillisecond)
{
if(residualI.length != I_OVERLAP || residualQ.length != Q_OVERLAP)

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,27 +17,27 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
/**
* Scalar implementation for non-packed Airspy native buffers
* Scalar implementation for non-packed native buffers
*/
public class AirspyBufferIteratorScalar extends AirspyBufferIterator<ComplexSamples>
public class SampleBufferIteratorScalar extends SampleBufferIterator<ComplexSamples>
{
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
public SampleBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc, long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
@@ -26,23 +26,23 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyBufferIteratorVector128Bits extends AirspyBufferIterator<ComplexSamples>
public class SampleBufferIteratorVector128Bits extends SampleBufferIterator<ComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
@@ -26,23 +26,23 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyBufferIteratorVector256Bits extends AirspyBufferIterator<ComplexSamples>
public class SampleBufferIteratorVector256Bits extends SampleBufferIterator<ComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
@@ -26,9 +26,9 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<ComplexSamples>
public class SampleBufferIteratorVector512Bits extends SampleBufferIterator<ComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
private float[] mFilterPart2;
@@ -36,14 +36,14 @@ public class AirspyBufferIteratorVector512Bits extends AirspyBufferIterator<Comp
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.ComplexSamples;
import java.util.Arrays;
@@ -26,23 +26,23 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyBufferIteratorVector64Bits extends AirspyBufferIterator<ComplexSamples>
public class SampleBufferIteratorVector64Bits extends SampleBufferIterator<ComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,26 +17,26 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
/**
* Scalar implementation for non-packed Airspy native buffers
* Scalar implementation for non-packed native buffers
*/
public class AirspyInterleavedBufferIteratorScalar extends AirspyBufferIterator<InterleavedComplexSamples>
public class SampleInterleavedBufferIteratorScalar extends SampleBufferIterator<InterleavedComplexSamples>
{
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleInterleavedBufferIteratorScalar(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import jdk.incubator.vector.FloatVector;
@@ -25,23 +25,23 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyInterleavedBufferIteratorVector128Bits extends AirspyBufferIterator<InterleavedComplexSamples>
public class SampleInterleavedBufferIteratorVector128Bits extends SampleBufferIterator<InterleavedComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_128;
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleInterleavedBufferIteratorVector128Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import jdk.incubator.vector.FloatVector;
@@ -25,23 +25,23 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyInterleavedBufferIteratorVector256Bits extends AirspyBufferIterator<InterleavedComplexSamples>
public class SampleInterleavedBufferIteratorVector256Bits extends SampleBufferIterator<InterleavedComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_256;
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleInterleavedBufferIteratorVector256Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import jdk.incubator.vector.FloatVector;
@@ -25,9 +25,9 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIterator<InterleavedComplexSamples>
public class SampleInterleavedBufferIteratorVector512Bits extends SampleBufferIterator<InterleavedComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_512;
private float[] mFilterPart2;
@@ -35,14 +35,14 @@ public class AirspyInterleavedBufferIteratorVector512Bits extends AirspyBufferIt
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleInterleavedBufferIteratorVector512Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2025 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.sample.complex.InterleavedComplexSamples;
import jdk.incubator.vector.FloatVector;
@@ -25,23 +25,23 @@ import jdk.incubator.vector.VectorOperators;
import jdk.incubator.vector.VectorSpecies;
/**
* Vector SIMD implementation for non-packed Airspy native buffers
* Vector SIMD implementation for non-packed native buffers
*/
public class AirspyInterleavedBufferIteratorVector64Bits extends AirspyBufferIterator<InterleavedComplexSamples>
public class SampleInterleavedBufferIteratorVector64Bits extends SampleBufferIterator<InterleavedComplexSamples>
{
private static final VectorSpecies<Float> VECTOR_SPECIES = FloatVector.SPECIES_64;
/**
* Constructs an instance
*
* @param samples from the airspy, either packed or unpacked.
* @param samples from the device, either packed or unpacked.
* @param residualI samples from last buffer
* @param residualQ samples from last buffer
* @param averageDc measured
* @param timestamp of the buffer
* @param samplesPerMillisecond to calculate sub-buffer fragment timestamps
*/
public AirspyInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleInterleavedBufferIteratorVector64Bits(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, float samplesPerMillisecond)
{
super(samples, residualI, residualQ, averageDc, timestamp, samplesPerMillisecond);

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,7 +17,7 @@
* ****************************************************************************
*/
package io.github.dsheirer.buffer.airspy;
package io.github.dsheirer.buffer.sample;
import io.github.dsheirer.buffer.AbstractNativeBuffer;
import io.github.dsheirer.sample.complex.ComplexSamples;
@@ -26,20 +26,20 @@ import io.github.dsheirer.vector.calibrate.Implementation;
import java.util.Iterator;
/**
* Native buffer scalar implementation for Airspy non-packed samples.
* Native buffer scalar implementation for non-packed samples.
*/
public class AirspyNativeBuffer extends AbstractNativeBuffer
public class SampleNativeBuffer extends AbstractNativeBuffer
{
private short[] mSamples;
private short[] mResidualI;
private short[] mResidualQ;
private float mAverageDc;
private Implementation mInterleavedImplementation;
private Implementation mNonInterleavedImplementation;
private final short[] mSamples;
private final short[] mResidualI;
private final short[] mResidualQ;
private final float mAverageDc;
private final Implementation mInterleavedImplementation;
private final Implementation mNonInterleavedImplementation;
/**
* Constructs an instance
* @param samples (non-packed) from the airspy device
* @param samples (non-packed) from the device
* @param residualI samples from previous buffer
* @param residualQ samples from previous buffer
* @param averageDc measured
@@ -48,17 +48,17 @@ public class AirspyNativeBuffer extends AbstractNativeBuffer
* @param nonInterleavedImplementation optimal, scalar vs vector SIMD
* @param samplesPerMillisecond used to calculate sub-buffer fragment timestamp offsets from the start of this buffer.
*/
public AirspyNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
public SampleNativeBuffer(short[] samples, short[] residualI, short[] residualQ, float averageDc,
long timestamp, Implementation interleavedImplementation,
Implementation nonInterleavedImplementation, float samplesPerMillisecond)
{
super(timestamp, samplesPerMillisecond);
//Ensure we're an even multiple of the fragment size. Typically, this will be 64k or 128k
if(samples.length % AirspyBufferIterator.FRAGMENT_SIZE != 0)
if(samples.length % SampleBufferIterator.FRAGMENT_SIZE != 0)
{
throw new IllegalArgumentException("Samples short[] length [" + samples.length +
"] must be an even multiple of " + AirspyBufferIterator.FRAGMENT_SIZE);
"] must be an even multiple of " + SampleBufferIterator.FRAGMENT_SIZE);
}
mSamples = samples;
@@ -78,13 +78,13 @@ public class AirspyNativeBuffer extends AbstractNativeBuffer
@Override
public Iterator<ComplexSamples> iterator()
{
return switch(mInterleavedImplementation)
return switch(mNonInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_512 -> new SampleBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new SampleBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new SampleBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new SampleBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new SampleBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}
@@ -93,11 +93,11 @@ public class AirspyNativeBuffer extends AbstractNativeBuffer
{
return switch(mInterleavedImplementation)
{
case VECTOR_SIMD_512 -> new AirspyInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new AirspyInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new AirspyInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new AirspyInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new AirspyInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_512 -> new SampleInterleavedBufferIteratorVector512Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_256-> new SampleInterleavedBufferIteratorVector256Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_128-> new SampleInterleavedBufferIteratorVector128Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
case VECTOR_SIMD_64 ->new SampleInterleavedBufferIteratorVector64Bits(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
default -> new SampleInterleavedBufferIteratorScalar(mSamples, mResidualI, mResidualQ, mAverageDc, getTimestamp(), getSamplesPerMillisecond());
};
}
}

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