ka9q-python 3.14.2__tar.gz → 3.15.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/CHANGELOG.md +61 -0
- {ka9q_python-3.14.2/ka9q_python.egg-info → ka9q_python-3.15.1}/PKG-INFO +18 -2
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/README.md +15 -1
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/API_REFERENCE.md +66 -15
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/ARCHITECTURE.md +11 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/INSTALLATION.md +20 -5
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/MULTI_STREAM.md +10 -4
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/__init__.py +9 -1
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/compat.py +1 -1
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/control.py +18 -146
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/multi_stream.py +10 -1
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/stream.py +149 -15
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/types.py +1 -1
- {ka9q_python-3.14.2 → ka9q_python-3.15.1/ka9q_python.egg-info}/PKG-INFO +18 -2
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q_python.egg-info/SOURCES.txt +1 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q_python.egg-info/requires.txt +3 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q_radio_compat +1 -1
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/pyproject.toml +4 -1
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_filter_edges.py +1 -0
- ka9q_python-3.15.1/tests/test_parse_rtp_samples_iq.py +118 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/LICENSE +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/MANIFEST.in +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/CLI_GUIDE.md +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/GETTING_STARTED.md +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/RECIPES.md +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/RTP_TIMING_SUPPORT.md +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/SECURITY.md +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/TESTING_GUIDE.md +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/docs/TUI_GUIDE.md +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/advanced_features_demo.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/channel_cleanup_example.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/codar_oceanography.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/diagnostics/diagnose_packets.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/diagnostics/repro_utc_bug.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/discover_example.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/grape_integration_example.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/hf_band_scanner.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/multi_stream_smoke.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/rtp_recorder_example.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/simple_am_radio.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/spectrum_example.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/stream_example.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/superdarn_recorder.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/test_channel_operations.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/test_improvements.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/test_timing_fields.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/tune.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/examples/tune_example.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/_multicast.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/addressing.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/cli.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/discovery.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/exceptions.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/managed_stream.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/monitor.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/pps_calibrator.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/resequencer.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/rtp_recorder.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/spectrum_stream.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/status.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/stream_quality.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/tui.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q/utils.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q_python.egg-info/dependency_links.txt +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q_python.egg-info/entry_points.txt +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/ka9q_python.egg-info/top_level.txt +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/scripts/check_upstream_drift.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/scripts/sync_types.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/setup.cfg +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/setup.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/__init__.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/conftest.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_addressing.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_channel_verification.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_client_id_destination.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_create_split_encoding.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_decode_description.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_decode_functions.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_encode_functions.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_encode_socket.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_ensure_channel_encoding.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_integration.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_iq_20khz_f32.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_lifetime.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_listen_multicast.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_managed_stream_recovery.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_monitor.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_multicast_helpers.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_multihomed.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_native_discovery.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_performance_fixes.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_protocol_compat.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_remove_channel.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_rtp_recorder.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_security_features.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_spectrum.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_ssrc_dest_unit.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_ssrc_encoding_unit.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_ssrc_radiod_host_unit.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_status_decoder.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_ttl_warning.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_tune.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_tune_cli.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_tune_debug.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_tune_live.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_tune_method.py +0 -0
- {ka9q_python-3.14.2 → ka9q_python-3.15.1}/tests/test_upstream_drift.py +0 -0
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [3.15.1] - 2026-05-21
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`ka9q.__version__` was stale.** v3.15.0's wheel correctly identified
|
|
8
|
+
itself as `3.15.0` in dist-info, but the `__version__` string baked
|
|
9
|
+
into `ka9q/__init__.py` still read `'3.14.2'` — the in-package
|
|
10
|
+
introspection answer was wrong for every consumer. The fix replaces
|
|
11
|
+
the hardcoded literal with `importlib.metadata.version("ka9q-python")`,
|
|
12
|
+
so `__version__` now always tracks the installed dist-info — drift
|
|
13
|
+
between release version and code-reported version is no longer
|
|
14
|
+
possible. Falls back to `"0.0.0+unknown"` on import error or when
|
|
15
|
+
the package is not installed (e.g. running from a source tree
|
|
16
|
+
without an editable install).
|
|
17
|
+
|
|
18
|
+
## [3.15.0] - 2026-05-21
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **F16LE / F16BE RTP payload decoding** in `parse_rtp_samples` for radiod's
|
|
23
|
+
float16 output mode (encodings 6 and 9). Both audio and IQ paths.
|
|
24
|
+
- **G.711 µ-law / A-law decoders** for encodings 10 and 11 — pure-numpy
|
|
25
|
+
table-based, no `audioop` dependency (which was removed in Python 3.13).
|
|
26
|
+
- **`OpusDecoder` class** in `ka9q.stream` for OPUS / OPUS_VOIP payloads
|
|
27
|
+
(encodings 3 and 7). Lazy-imports `opuslib`; install via
|
|
28
|
+
`pip install ka9q-python[opus]`. Maintains codec state across calls so
|
|
29
|
+
packet-loss concealment works end-to-end — one instance per stream SSRC.
|
|
30
|
+
- New `opus` optional dependency in `pyproject.toml` (`opuslib>=3.0`).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **`RadiodControl.set_agc(attack_rate=...)` was unreachable.** It encoded
|
|
35
|
+
`StatusType.AGC_ATTACK_RATE`, which does not exist in ka9q-radio's
|
|
36
|
+
`status.h` or in `ka9q/types.py` — any call passing `attack_rate=` raised
|
|
37
|
+
`AttributeError`. Replaced with a working `threshold:` kwarg backed by
|
|
38
|
+
`AGC_THRESHOLD` (which radiod actually decodes in
|
|
39
|
+
`decode_radio_commands()`).
|
|
40
|
+
- **Removed 6 stale duplicate `set_*` methods.** In a single class, second
|
|
41
|
+
defs silently shadow firsts — so callers were already using the correct
|
|
42
|
+
versions further down the file. The dead first defs of `set_squelch`,
|
|
43
|
+
`set_pll`, `set_output_channels`, `set_independent_sideband`,
|
|
44
|
+
`set_envelope_detection`, and `set_opus_bitrate` are gone. One of them
|
|
45
|
+
(`set_opus_bitrate`) referenced a non-existent `StatusType.OPUS_BITRATE`
|
|
46
|
+
— the working second def uses the correct `OPUS_BIT_RATE`.
|
|
47
|
+
|
|
48
|
+
### Tooling / pinning
|
|
49
|
+
|
|
50
|
+
- Pin advanced from ka9q-radio `f78cff9c` (1.0.0-22) to `d555f1853422`.
|
|
51
|
+
Drift report confirms zero TLV-tag, encoding-enum, demod-type, or
|
|
52
|
+
window-type changes across the 68 intervening upstream commits — only
|
|
53
|
+
packaging, hydrasdr-driver, and internal C-struct refactors. Existing
|
|
54
|
+
ka9q-python state (`types.py`, `decode_status_packet`, `SpectrumStream`,
|
|
55
|
+
every `set_*` method) was already covering the full HEAD protocol
|
|
56
|
+
surface; this release brings the recorded compatibility tag in line.
|
|
57
|
+
|
|
58
|
+
### Tests
|
|
59
|
+
|
|
60
|
+
- Fixed pre-existing `tests/test_filter_edges.py::_bare_control` helper
|
|
61
|
+
that did not initialise the `client_id` attribute added with v3.14.0
|
|
62
|
+
per-(client,radiod) multicast destinations.
|
|
63
|
+
|
|
3
64
|
## [3.14.2] - 2026-05-14
|
|
4
65
|
|
|
5
66
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ka9q-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.15.1
|
|
4
4
|
Summary: Python interface for ka9q-radio control and monitoring
|
|
5
5
|
Home-page: https://github.com/mijahauan/ka9q-python
|
|
6
6
|
Author: Michael Hauan AC0G
|
|
@@ -30,6 +30,8 @@ Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
|
30
30
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
31
31
|
Provides-Extra: tui
|
|
32
32
|
Requires-Dist: textual>=0.50; extra == "tui"
|
|
33
|
+
Provides-Extra: opus
|
|
34
|
+
Requires-Dist: opuslib>=3.0; extra == "opus"
|
|
33
35
|
Dynamic: author
|
|
34
36
|
Dynamic: home-page
|
|
35
37
|
Dynamic: license-file
|
|
@@ -60,7 +62,8 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
60
62
|
|
|
61
63
|
## Features
|
|
62
64
|
|
|
63
|
-
- **Complete radiod API** — all
|
|
65
|
+
- **Complete radiod API** — all 117 TLV status/command parameters exposed, generated from ka9q-radio's C headers
|
|
66
|
+
- **Every radiod RTP encoding decoded** — `S16LE/BE`, `F32LE/BE`, `F16LE/BE`, `MULAW`, `ALAW` via pure-NumPy `parse_rtp_samples()`; `OPUS` / `OPUS_VOIP` via the optional `OpusDecoder` (install with `[opus]` extra)
|
|
64
67
|
- **Four stream abstractions** — `RTPRecorder` (raw packets), `RadiodStream` (samples + gap handling), `ManagedStream` (self-healing single channel), `MultiStream` (shared socket, many SSRCs)
|
|
65
68
|
- **Typed status decoder** — `ChannelStatus`, `FrontendStatus`, `PllStatus`, etc. with dotted-path field access
|
|
66
69
|
- **Precise RTP timing** — GPS_TIME / RTP_TIMESNAP for sample-accurate wallclock timestamps
|
|
@@ -76,6 +79,19 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
76
79
|
pip install ka9q-python
|
|
77
80
|
```
|
|
78
81
|
|
|
82
|
+
Optional extras:
|
|
83
|
+
|
|
84
|
+
| Extra | Adds | Needed for |
|
|
85
|
+
|-------|------|------------|
|
|
86
|
+
| `tui` | `textual` | `ka9q tui` interactive terminal UI |
|
|
87
|
+
| `opus` | `opuslib` | decoding `OPUS` / `OPUS_VOIP` RTP payloads via `OpusDecoder` |
|
|
88
|
+
| `dev` | `pytest`, `pytest-cov` | running the test suite |
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install "ka9q-python[opus]" # one extra
|
|
92
|
+
pip install "ka9q-python[tui,opus]" # multiple
|
|
93
|
+
```
|
|
94
|
+
|
|
79
95
|
Or install from source:
|
|
80
96
|
|
|
81
97
|
```bash
|
|
@@ -23,7 +23,8 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
23
23
|
|
|
24
24
|
## Features
|
|
25
25
|
|
|
26
|
-
- **Complete radiod API** — all
|
|
26
|
+
- **Complete radiod API** — all 117 TLV status/command parameters exposed, generated from ka9q-radio's C headers
|
|
27
|
+
- **Every radiod RTP encoding decoded** — `S16LE/BE`, `F32LE/BE`, `F16LE/BE`, `MULAW`, `ALAW` via pure-NumPy `parse_rtp_samples()`; `OPUS` / `OPUS_VOIP` via the optional `OpusDecoder` (install with `[opus]` extra)
|
|
27
28
|
- **Four stream abstractions** — `RTPRecorder` (raw packets), `RadiodStream` (samples + gap handling), `ManagedStream` (self-healing single channel), `MultiStream` (shared socket, many SSRCs)
|
|
28
29
|
- **Typed status decoder** — `ChannelStatus`, `FrontendStatus`, `PllStatus`, etc. with dotted-path field access
|
|
29
30
|
- **Precise RTP timing** — GPS_TIME / RTP_TIMESNAP for sample-accurate wallclock timestamps
|
|
@@ -39,6 +40,19 @@ Control radiod channels for any application: AM/FM/SSB radio, WSPR monitoring, S
|
|
|
39
40
|
pip install ka9q-python
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
Optional extras:
|
|
44
|
+
|
|
45
|
+
| Extra | Adds | Needed for |
|
|
46
|
+
|-------|------|------------|
|
|
47
|
+
| `tui` | `textual` | `ka9q tui` interactive terminal UI |
|
|
48
|
+
| `opus` | `opuslib` | decoding `OPUS` / `OPUS_VOIP` RTP payloads via `OpusDecoder` |
|
|
49
|
+
| `dev` | `pytest`, `pytest-cov` | running the test suite |
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install "ka9q-python[opus]" # one extra
|
|
53
|
+
pip install "ka9q-python[tui,opus]" # multiple
|
|
54
|
+
```
|
|
55
|
+
|
|
42
56
|
Or install from source:
|
|
43
57
|
|
|
44
58
|
```bash
|
|
@@ -150,7 +150,7 @@ Tuning & filter:
|
|
|
150
150
|
|
|
151
151
|
Gain / AGC:
|
|
152
152
|
- `set_gain(ssrc, gain_db)`
|
|
153
|
-
- `set_agc(ssrc, enable, hangtime=None, recovery_rate=None, threshold=None)`
|
|
153
|
+
- `set_agc(ssrc, enable, hangtime=None, headroom=None, recovery_rate=None, threshold=None)`
|
|
154
154
|
- `set_agc_hangtime(ssrc, seconds)`
|
|
155
155
|
- `set_agc_recovery_rate(ssrc, db_per_sec)`
|
|
156
156
|
- `set_agc_threshold(ssrc, threshold_db)`
|
|
@@ -286,20 +286,20 @@ for the full list — they mirror ka9q-radio's `status.h`.
|
|
|
286
286
|
|
|
287
287
|
RTP output encoding (integer values match ka9q-radio's `rtp.h`):
|
|
288
288
|
|
|
289
|
-
| Name | Value | Notes |
|
|
290
|
-
|
|
291
|
-
| `NO_ENCODING` | 0 | radiod default |
|
|
292
|
-
| `S16LE` | 1 | Signed 16-bit little-endian PCM |
|
|
293
|
-
| `S16BE` | 2 | Signed 16-bit big-endian PCM |
|
|
294
|
-
| `OPUS` | 3 | Opus audio |
|
|
295
|
-
| `F32LE` | 4 | 32-bit float LE (also `Encoding.F32`) |
|
|
296
|
-
| `AX25` | 5 | Packet radio |
|
|
297
|
-
| `F16LE` | 6 | 16-bit float LE (also `Encoding.F16`) |
|
|
298
|
-
| `OPUS_VOIP` | 7 | Opus with `APPLICATION_VOIP` |
|
|
299
|
-
| `F32BE` | 8 | 32-bit float BE |
|
|
300
|
-
| `F16BE` | 9 | 16-bit float BE |
|
|
301
|
-
| `MULAW` | 10 |
|
|
302
|
-
| `ALAW` | 11 | A-law |
|
|
289
|
+
| Name | Value | Decoded by | Notes |
|
|
290
|
+
|---|---|---|---|
|
|
291
|
+
| `NO_ENCODING` | 0 | `parse_rtp_samples` | radiod default → treated as F32LE |
|
|
292
|
+
| `S16LE` | 1 | `parse_rtp_samples` | Signed 16-bit little-endian PCM |
|
|
293
|
+
| `S16BE` | 2 | `parse_rtp_samples` | Signed 16-bit big-endian PCM |
|
|
294
|
+
| `OPUS` | 3 | `OpusDecoder` (extra) | Opus audio — needs `[opus]` install extra |
|
|
295
|
+
| `F32LE` | 4 | `parse_rtp_samples` | 32-bit float LE (also `Encoding.F32`) |
|
|
296
|
+
| `AX25` | 5 | (caller handles bytes) | Packet radio — framed protocol, not samples |
|
|
297
|
+
| `F16LE` | 6 | `parse_rtp_samples` | 16-bit float LE (also `Encoding.F16`) |
|
|
298
|
+
| `OPUS_VOIP` | 7 | `OpusDecoder` (extra) | Opus with `APPLICATION_VOIP` |
|
|
299
|
+
| `F32BE` | 8 | `parse_rtp_samples` | 32-bit float BE |
|
|
300
|
+
| `F16BE` | 9 | `parse_rtp_samples` | 16-bit float BE |
|
|
301
|
+
| `MULAW` | 10 | `parse_rtp_samples` | G.711 µ-law (table-based, pure NumPy) |
|
|
302
|
+
| `ALAW` | 11 | `parse_rtp_samples` | G.711 A-law (table-based, pure NumPy) |
|
|
303
303
|
|
|
304
304
|
### `DemodType`
|
|
305
305
|
|
|
@@ -417,6 +417,57 @@ Derived properties:
|
|
|
417
417
|
|
|
418
418
|
## Streams
|
|
419
419
|
|
|
420
|
+
### `parse_rtp_samples()`
|
|
421
|
+
|
|
422
|
+
Source: [stream.py](../ka9q/stream.py). Decodes one RTP payload to a
|
|
423
|
+
NumPy sample array. Shared by `RadiodStream` and `MultiStream`.
|
|
424
|
+
|
|
425
|
+
```python
|
|
426
|
+
parse_rtp_samples(
|
|
427
|
+
payload: bytes,
|
|
428
|
+
encoding: int, # any Encoding.* value
|
|
429
|
+
is_iq: bool, # True → complex64 output; False → float32
|
|
430
|
+
) -> np.ndarray | None
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Covers every linear-PCM encoding radiod's `pt_from_info()` can grant:
|
|
434
|
+
`NO_ENCODING`, `S16LE`, `S16BE`, `F32LE`, `F32BE`, `F16LE`, `F16BE`,
|
|
435
|
+
`MULAW`, `ALAW`. G.711 µ-law / A-law use table-based NumPy decoders
|
|
436
|
+
(no `audioop` dependency — that module is removed in Python 3.13).
|
|
437
|
+
Unsupported encodings log a warning and fall back to F32LE.
|
|
438
|
+
|
|
439
|
+
Returns `None` for `OPUS`, `OPUS_VOIP` (use `OpusDecoder` below), and
|
|
440
|
+
`AX25` (framed protocol data, handle the bytes yourself).
|
|
441
|
+
|
|
442
|
+
### `OpusDecoder`
|
|
443
|
+
|
|
444
|
+
Source: [stream.py](../ka9q/stream.py). Optional Opus payload decoder
|
|
445
|
+
for radiod's `OPUS` / `OPUS_VOIP` streams. Requires the `[opus]`
|
|
446
|
+
install extra (`pip install ka9q-python[opus]`, which pulls
|
|
447
|
+
`opuslib`).
|
|
448
|
+
|
|
449
|
+
```python
|
|
450
|
+
OpusDecoder(sample_rate: int = 48000, channels: int = 1)
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
- `decode(payload: bytes, *, fec: bool = False) -> np.ndarray` —
|
|
454
|
+
one Opus frame → float32 PCM, normalised to ±1.0. Stereo output is
|
|
455
|
+
interleaved L,R,L,R,…. Empty payload triggers one frame of
|
|
456
|
+
packet-loss concealment.
|
|
457
|
+
- `sample_rate` / `channels` properties expose the codec config.
|
|
458
|
+
|
|
459
|
+
Maintain one `OpusDecoder` instance per stream SSRC so the internal
|
|
460
|
+
codec state — and therefore PLC — works correctly across packets.
|
|
461
|
+
`sample_rate` must be one of 8000/12000/16000/24000/48000.
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
from ka9q.stream import OpusDecoder
|
|
465
|
+
|
|
466
|
+
dec = OpusDecoder(sample_rate=48000, channels=1)
|
|
467
|
+
for payload in opus_payloads:
|
|
468
|
+
samples = dec.decode(payload) # float32, mono
|
|
469
|
+
```
|
|
470
|
+
|
|
420
471
|
### `RadiodStream`
|
|
421
472
|
|
|
422
473
|
Source: [stream.py](../ka9q/stream.py). Continuous-sample consumer
|
|
@@ -77,6 +77,17 @@ to handle out-of-order RTP. Hands each batch to an
|
|
|
77
77
|
`on_samples(samples, quality)` callback. Does **not** heal radiod
|
|
78
78
|
restarts — a restart produces a lasting silence.
|
|
79
79
|
|
|
80
|
+
`stream.py` also exposes two payload-decoder helpers shared by every
|
|
81
|
+
stream class:
|
|
82
|
+
|
|
83
|
+
- `parse_rtp_samples(payload, encoding, is_iq)` — decodes every
|
|
84
|
+
linear-PCM encoding radiod emits (`S16LE/BE`, `F32LE/BE`,
|
|
85
|
+
`F16LE/BE`, `MULAW`, `ALAW`) to NumPy in pure Python, no
|
|
86
|
+
`audioop` (gone in Py 3.13).
|
|
87
|
+
- `OpusDecoder(sample_rate, channels)` — decodes `OPUS` / `OPUS_VOIP`
|
|
88
|
+
via the optional `opuslib` dependency (`pip install ka9q-python[opus]`).
|
|
89
|
+
One instance per SSRC so codec state and PLC stay coherent.
|
|
90
|
+
|
|
80
91
|
### 3. `ManagedStream` ([managed_stream.py](../ka9q/managed_stream.py))
|
|
81
92
|
|
|
82
93
|
High-level self-healing wrapper around `RadiodStream`. A background
|
|
@@ -41,6 +41,18 @@ pip install -e ".[dev]"
|
|
|
41
41
|
|
|
42
42
|
This includes pytest and other testing tools.
|
|
43
43
|
|
|
44
|
+
### Optional Extras
|
|
45
|
+
|
|
46
|
+
`ka9q-python` defines three optional dependency groups:
|
|
47
|
+
|
|
48
|
+
| Extra | Adds | Needed for |
|
|
49
|
+
|-------|------|------------|
|
|
50
|
+
| `dev` | `pytest`, `pytest-cov` | running the test suite |
|
|
51
|
+
| `tui` | `textual` | the `ka9q tui` interactive terminal UI |
|
|
52
|
+
| `opus` | `opuslib` | decoding `OPUS` / `OPUS_VOIP` RTP payloads with `ka9q.stream.OpusDecoder` |
|
|
53
|
+
|
|
54
|
+
Combine as needed, e.g. `pip install -e ".[dev,opus]"`.
|
|
55
|
+
|
|
44
56
|
## For Use in Other Applications
|
|
45
57
|
|
|
46
58
|
### In requirements.txt
|
|
@@ -98,7 +110,7 @@ python3 -c "import ka9q; print(f'ka9q version {ka9q.__version__} installed succe
|
|
|
98
110
|
|
|
99
111
|
Expected output (version will match whatever is installed):
|
|
100
112
|
```
|
|
101
|
-
ka9q version 3.
|
|
113
|
+
ka9q version 3.15.0 installed successfully
|
|
102
114
|
```
|
|
103
115
|
|
|
104
116
|
After installation, the `ka9q` command-line tool is also on your `PATH`:
|
|
@@ -131,7 +143,8 @@ Package works with Python's built-in networking. Use IP addresses instead of `.l
|
|
|
131
143
|
- `numpy >= 1.24.0`
|
|
132
144
|
|
|
133
145
|
### Optional (for enhanced functionality)
|
|
134
|
-
- `textual` — required for `ka9q tui`.
|
|
146
|
+
- `textual` — required for `ka9q tui`. Install via the `[tui]` extra or `pip install textual` directly.
|
|
147
|
+
- `opuslib` — required to decode `OPUS` / `OPUS_VOIP` RTP payloads via `ka9q.stream.OpusDecoder`. Install via the `[opus]` extra or `pip install opuslib`. (Linear-PCM encodings — `S16LE/BE`, `F32LE/BE`, `F16LE/BE`, `MULAW`, `ALAW` — are decoded in pure NumPy and need no extra dependency.)
|
|
135
148
|
- `avahi-utils` (Linux) — faster mDNS hostname resolution
|
|
136
149
|
- `control` from ka9q-radio — fallback path for channel discovery
|
|
137
150
|
|
|
@@ -187,11 +200,13 @@ pip uninstall ka9q
|
|
|
187
200
|
```bash
|
|
188
201
|
pip install build
|
|
189
202
|
python3 -m build
|
|
203
|
+
# or, with uv:
|
|
204
|
+
uv build
|
|
190
205
|
```
|
|
191
206
|
|
|
192
|
-
This creates:
|
|
193
|
-
- `dist/
|
|
194
|
-
- `dist/
|
|
207
|
+
This creates (filenames track the current version in `pyproject.toml`):
|
|
208
|
+
- `dist/ka9q_python-X.Y.Z-py3-none-any.whl` (wheel)
|
|
209
|
+
- `dist/ka9q_python-X.Y.Z.tar.gz` (source distribution)
|
|
195
210
|
|
|
196
211
|
### Upload to PyPI (maintainers only)
|
|
197
212
|
```bash
|
|
@@ -254,10 +254,16 @@ Based on the current source ([`ka9q/multi_stream.py`](../ka9q/multi_stream.py)):
|
|
|
254
254
|
- **No `remove_channel()` in the public API.** To remove a channel,
|
|
255
255
|
stop the `MultiStream`, rebuild it with the remaining channels, and
|
|
256
256
|
start again. For long-lived recorders this has been acceptable.
|
|
257
|
-
- **
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
- **Linear-PCM encodings** are decoded by `parse_rtp_samples()` in
|
|
258
|
+
pure NumPy: `S16LE/BE`, `F32LE/BE`, `F16LE/BE`, `MULAW`, `ALAW`.
|
|
259
|
+
All radiod-emitted sample encodings work out of the box.
|
|
260
|
+
- **Opus** (`OPUS`, `OPUS_VOIP`) is a framed codec, not raw samples
|
|
261
|
+
— `parse_rtp_samples()` returns `None` for those payloads. Wrap
|
|
262
|
+
the stream with `ka9q.stream.OpusDecoder` (requires the `[opus]`
|
|
263
|
+
install extra) to recover float32 PCM, keeping one decoder
|
|
264
|
+
instance per SSRC so packet-loss concealment works.
|
|
265
|
+
- **AX25** (encoding 5) is framed protocol data, also not samples —
|
|
266
|
+
`parse_rtp_samples()` returns `None`. Handle the bytes yourself.
|
|
261
267
|
- **`samples_per_packet=320` default** assumes the typical 12 kHz /
|
|
262
268
|
26.67 ms RTP packetization used by radiod. If your channels run
|
|
263
269
|
at a different packet cadence, set it explicitly.
|
|
@@ -56,7 +56,15 @@ Lower-level usage (explicit control):
|
|
|
56
56
|
)
|
|
57
57
|
print(f"Created channel with SSRC: {ssrc}")
|
|
58
58
|
"""
|
|
59
|
-
|
|
59
|
+
try:
|
|
60
|
+
from importlib.metadata import version as _pkg_version, PackageNotFoundError as _PkgNotFound
|
|
61
|
+
try:
|
|
62
|
+
__version__ = _pkg_version("ka9q-python")
|
|
63
|
+
except _PkgNotFound:
|
|
64
|
+
__version__ = "0.0.0+unknown"
|
|
65
|
+
del _pkg_version, _PkgNotFound
|
|
66
|
+
except ImportError:
|
|
67
|
+
__version__ = "0.0.0+unknown"
|
|
60
68
|
__author__ = 'Michael Hauan AC0G'
|
|
61
69
|
|
|
62
70
|
from .control import RadiodControl, allocate_ssrc
|
|
@@ -973,23 +973,27 @@ class RadiodControl:
|
|
|
973
973
|
logger.info(f"Setting sample rate for SSRC {ssrc} to {sample_rate} Hz")
|
|
974
974
|
self.send_command(cmdbuffer)
|
|
975
975
|
|
|
976
|
-
def set_agc(self, ssrc: int, enable: bool, hangtime: Optional[float] = None,
|
|
976
|
+
def set_agc(self, ssrc: int, enable: bool, hangtime: Optional[float] = None,
|
|
977
977
|
headroom: Optional[float] = None, recovery_rate: Optional[float] = None,
|
|
978
|
-
|
|
978
|
+
threshold: Optional[float] = None):
|
|
979
979
|
"""
|
|
980
|
-
Configure AGC (Automatic Gain Control) for a channel
|
|
981
|
-
|
|
980
|
+
Configure AGC (Automatic Gain Control) for a channel.
|
|
981
|
+
|
|
982
|
+
radiod exposes four AGC knobs: enable, hangtime, headroom, recovery_rate,
|
|
983
|
+
and threshold. There is no separate "attack rate" — see radio_status.c
|
|
984
|
+
decode_radio_commands().
|
|
985
|
+
|
|
982
986
|
Args:
|
|
983
987
|
ssrc: SSRC of the channel
|
|
984
988
|
enable: Enable/disable AGC (True=enabled, False=manual gain)
|
|
985
|
-
hangtime: AGC hang time in seconds
|
|
986
|
-
headroom: Target headroom in dB (
|
|
987
|
-
recovery_rate: AGC recovery rate
|
|
988
|
-
|
|
989
|
+
hangtime: AGC hang time in seconds
|
|
990
|
+
headroom: Target headroom in dB (positive; radiod negates internally)
|
|
991
|
+
recovery_rate: AGC recovery rate in dB/sec
|
|
992
|
+
threshold: AGC threshold in dB
|
|
989
993
|
"""
|
|
990
994
|
cmdbuffer = bytearray()
|
|
991
995
|
cmdbuffer.append(CMD)
|
|
992
|
-
|
|
996
|
+
|
|
993
997
|
encode_int(cmdbuffer, StatusType.AGC_ENABLE, 1 if enable else 0)
|
|
994
998
|
if hangtime is not None:
|
|
995
999
|
encode_float(cmdbuffer, StatusType.AGC_HANGTIME, hangtime)
|
|
@@ -997,14 +1001,14 @@ class RadiodControl:
|
|
|
997
1001
|
encode_float(cmdbuffer, StatusType.HEADROOM, headroom)
|
|
998
1002
|
if recovery_rate is not None:
|
|
999
1003
|
encode_float(cmdbuffer, StatusType.AGC_RECOVERY_RATE, recovery_rate)
|
|
1000
|
-
if
|
|
1001
|
-
encode_float(cmdbuffer, StatusType.
|
|
1002
|
-
|
|
1004
|
+
if threshold is not None:
|
|
1005
|
+
encode_float(cmdbuffer, StatusType.AGC_THRESHOLD, threshold)
|
|
1006
|
+
|
|
1003
1007
|
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
1004
1008
|
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
1005
1009
|
encode_eol(cmdbuffer)
|
|
1006
|
-
|
|
1007
|
-
logger.info(f"Setting AGC for SSRC {ssrc}: enable={enable}
|
|
1010
|
+
|
|
1011
|
+
logger.info(f"Setting AGC for SSRC {ssrc}: enable={enable}")
|
|
1008
1012
|
self.send_command(cmdbuffer)
|
|
1009
1013
|
|
|
1010
1014
|
def set_gain(self, ssrc: int, gain_db: float):
|
|
@@ -1710,138 +1714,6 @@ class RadiodControl:
|
|
|
1710
1714
|
logger.info(f"Setting LIFETIME for SSRC {ssrc} to {lifetime} frames")
|
|
1711
1715
|
self.send_command(cmdbuffer)
|
|
1712
1716
|
|
|
1713
|
-
def set_squelch(self, ssrc: int, open_threshold: Optional[float] = None,
|
|
1714
|
-
close_threshold: Optional[float] = None, snr_squelch: Optional[bool] = None):
|
|
1715
|
-
"""
|
|
1716
|
-
Set squelch parameters for a channel
|
|
1717
|
-
|
|
1718
|
-
Args:
|
|
1719
|
-
ssrc: SSRC of the channel
|
|
1720
|
-
open_threshold: Squelch open threshold in dB (optional)
|
|
1721
|
-
close_threshold: Squelch close threshold in dB (optional)
|
|
1722
|
-
snr_squelch: Enable SNR-based squelch (optional)
|
|
1723
|
-
"""
|
|
1724
|
-
cmdbuffer = bytearray()
|
|
1725
|
-
cmdbuffer.append(CMD)
|
|
1726
|
-
|
|
1727
|
-
if open_threshold is not None:
|
|
1728
|
-
encode_float(cmdbuffer, StatusType.SQUELCH_OPEN, open_threshold)
|
|
1729
|
-
if close_threshold is not None:
|
|
1730
|
-
encode_float(cmdbuffer, StatusType.SQUELCH_CLOSE, close_threshold)
|
|
1731
|
-
if snr_squelch is not None:
|
|
1732
|
-
encode_int(cmdbuffer, StatusType.SNR_SQUELCH, 1 if snr_squelch else 0)
|
|
1733
|
-
|
|
1734
|
-
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
1735
|
-
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
1736
|
-
encode_eol(cmdbuffer)
|
|
1737
|
-
|
|
1738
|
-
logger.info(f"Setting squelch for SSRC {ssrc}")
|
|
1739
|
-
self.send_command(cmdbuffer)
|
|
1740
|
-
|
|
1741
|
-
def set_pll(self, ssrc: int, enable: Optional[bool] = None,
|
|
1742
|
-
bandwidth: Optional[float] = None, square: Optional[bool] = None):
|
|
1743
|
-
"""
|
|
1744
|
-
Set PLL parameters for a channel
|
|
1745
|
-
|
|
1746
|
-
Args:
|
|
1747
|
-
ssrc: SSRC of the channel
|
|
1748
|
-
enable: Enable/disable PLL (optional)
|
|
1749
|
-
bandwidth: PLL bandwidth in Hz (optional)
|
|
1750
|
-
square: Enable square-wave PLL output (optional)
|
|
1751
|
-
"""
|
|
1752
|
-
cmdbuffer = bytearray()
|
|
1753
|
-
cmdbuffer.append(CMD)
|
|
1754
|
-
|
|
1755
|
-
if enable is not None:
|
|
1756
|
-
encode_int(cmdbuffer, StatusType.PLL_ENABLE, 1 if enable else 0)
|
|
1757
|
-
if bandwidth is not None:
|
|
1758
|
-
encode_float(cmdbuffer, StatusType.PLL_BW, bandwidth)
|
|
1759
|
-
if square is not None:
|
|
1760
|
-
encode_int(cmdbuffer, StatusType.PLL_SQUARE, 1 if square else 0)
|
|
1761
|
-
|
|
1762
|
-
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
1763
|
-
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
1764
|
-
encode_eol(cmdbuffer)
|
|
1765
|
-
|
|
1766
|
-
logger.info(f"Setting PLL for SSRC {ssrc}")
|
|
1767
|
-
self.send_command(cmdbuffer)
|
|
1768
|
-
|
|
1769
|
-
def set_output_channels(self, ssrc: int, channels: int):
|
|
1770
|
-
"""
|
|
1771
|
-
Set number of output channels (mono/stereo)
|
|
1772
|
-
|
|
1773
|
-
Args:
|
|
1774
|
-
ssrc: SSRC of the channel
|
|
1775
|
-
channels: Number of channels (1=mono, 2=stereo)
|
|
1776
|
-
"""
|
|
1777
|
-
cmdbuffer = bytearray()
|
|
1778
|
-
cmdbuffer.append(CMD)
|
|
1779
|
-
|
|
1780
|
-
encode_int(cmdbuffer, StatusType.OUTPUT_CHANNELS, channels)
|
|
1781
|
-
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
1782
|
-
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
1783
|
-
encode_eol(cmdbuffer)
|
|
1784
|
-
|
|
1785
|
-
logger.info(f"Setting output channels for SSRC {ssrc} to {channels}")
|
|
1786
|
-
self.send_command(cmdbuffer)
|
|
1787
|
-
|
|
1788
|
-
def set_independent_sideband(self, ssrc: int, enable: bool):
|
|
1789
|
-
"""
|
|
1790
|
-
Enable/disable independent sideband (ISB) mode
|
|
1791
|
-
|
|
1792
|
-
Args:
|
|
1793
|
-
ssrc: SSRC of the channel
|
|
1794
|
-
enable: Enable ISB mode
|
|
1795
|
-
"""
|
|
1796
|
-
cmdbuffer = bytearray()
|
|
1797
|
-
cmdbuffer.append(CMD)
|
|
1798
|
-
|
|
1799
|
-
encode_int(cmdbuffer, StatusType.INDEPENDENT_SIDEBAND, 1 if enable else 0)
|
|
1800
|
-
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
1801
|
-
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
1802
|
-
encode_eol(cmdbuffer)
|
|
1803
|
-
|
|
1804
|
-
logger.info(f"Setting ISB for SSRC {ssrc} to {enable}")
|
|
1805
|
-
self.send_command(cmdbuffer)
|
|
1806
|
-
|
|
1807
|
-
def set_envelope_detection(self, ssrc: int, enable: bool):
|
|
1808
|
-
"""
|
|
1809
|
-
Enable/disable envelope detection for AM
|
|
1810
|
-
|
|
1811
|
-
Args:
|
|
1812
|
-
ssrc: SSRC of the channel
|
|
1813
|
-
enable: Enable envelope detection
|
|
1814
|
-
"""
|
|
1815
|
-
cmdbuffer = bytearray()
|
|
1816
|
-
cmdbuffer.append(CMD)
|
|
1817
|
-
|
|
1818
|
-
encode_int(cmdbuffer, StatusType.ENVELOPE, 1 if enable else 0)
|
|
1819
|
-
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
1820
|
-
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
1821
|
-
encode_eol(cmdbuffer)
|
|
1822
|
-
|
|
1823
|
-
logger.info(f"Setting envelope detection for SSRC {ssrc} to {enable}")
|
|
1824
|
-
self.send_command(cmdbuffer)
|
|
1825
|
-
|
|
1826
|
-
def set_opus_bitrate(self, ssrc: int, bitrate: int):
|
|
1827
|
-
"""
|
|
1828
|
-
Set Opus codec bitrate
|
|
1829
|
-
|
|
1830
|
-
Args:
|
|
1831
|
-
ssrc: SSRC of the channel
|
|
1832
|
-
bitrate: Bitrate in bits per second (6000-510000)
|
|
1833
|
-
"""
|
|
1834
|
-
cmdbuffer = bytearray()
|
|
1835
|
-
cmdbuffer.append(CMD)
|
|
1836
|
-
|
|
1837
|
-
encode_int(cmdbuffer, StatusType.OPUS_BITRATE, bitrate)
|
|
1838
|
-
encode_int(cmdbuffer, StatusType.OUTPUT_SSRC, ssrc)
|
|
1839
|
-
encode_int(cmdbuffer, StatusType.COMMAND_TAG, secrets.randbits(31))
|
|
1840
|
-
encode_eol(cmdbuffer)
|
|
1841
|
-
|
|
1842
|
-
logger.info(f"Setting Opus bitrate for SSRC {ssrc} to {bitrate}")
|
|
1843
|
-
self.send_command(cmdbuffer)
|
|
1844
|
-
|
|
1845
1717
|
def _setup_status_listener(self):
|
|
1846
1718
|
"""Set up socket to listen for status responses"""
|
|
1847
1719
|
# Create a separate socket for receiving status messages
|
|
@@ -201,12 +201,21 @@ class MultiStream:
|
|
|
201
201
|
ssrc = channel_info.ssrc
|
|
202
202
|
is_iq = preset.lower() in ("iq", "spectrum")
|
|
203
203
|
|
|
204
|
+
# Use the encoding radiod actually granted (channel_info.encoding)
|
|
205
|
+
# rather than the encoding we requested. radiod silently downgrades
|
|
206
|
+
# F32 → S16 for some IQ-channel configurations; storing the requested
|
|
207
|
+
# value caused parse_rtp_samples to decode upstream bytes with the
|
|
208
|
+
# wrong dtype and produce NaN-poisoned garbage. The granted encoding
|
|
209
|
+
# is authoritative. Fall back to the caller's value if channel_info
|
|
210
|
+
# doesn't carry an encoding (or carries 0 = "none/default").
|
|
211
|
+
granted_encoding = getattr(channel_info, 'encoding', 0) or encoding
|
|
212
|
+
|
|
204
213
|
slot = _ChannelSlot(
|
|
205
214
|
channel_info=channel_info,
|
|
206
215
|
frequency_hz=frequency_hz,
|
|
207
216
|
preset=preset,
|
|
208
217
|
sample_rate=sample_rate,
|
|
209
|
-
encoding=
|
|
218
|
+
encoding=granted_encoding,
|
|
210
219
|
is_iq=is_iq,
|
|
211
220
|
resequencer=PacketResequencer(
|
|
212
221
|
buffer_size=self._resequence_buffer_size,
|