op25-container
- generate-config takes p25_mode "conventional" + conv_channels [{frequency,
name, nac}]. It builds fixed fsk4 channels (up to 2 on one non-tunable SDR,
DC spike kept off the channels) with no control channel to hunt. Two
channels send audio to UDP 23456/23457, and audio.py -2 + amplify(2) keeps
levels.
- tk_drb_conv.py (a tk_p25.rx_ctl subclass, copied into OP25 apps) reports
voice / NAC / encryption (LC service options + LDU2 ESS algid/keyid) for
conventional channels and applies the per-channel NAC filter, which stock
tk_p25 does not. Analog uses it too.
- Trunked P25 config is unchanged when p25_mode is absent.
drb-edge-node
- radio_config: system kind from type + config.mode (missing = trunked).
- conventional.py normalizes the console: a distinctive LC TGID, else the
channel's last TGID (30 s), else a synthetic per-channel id (freq/100 Hz).
Encrypted rows are never talkers. With exactly one conventional channel,
orphan audio is attributed to it at close, which fixes analog recording
(#20). The synthetic id and the real TGID on one channel never split.
- Health/dashboard: P25 conventional listen tab (/api/listen p25conv), and
conventional decode + encryption checks. The checkin carries radio_channels.
- Tests: edge 296 pass; op25-container 18 (new CI job). Reviewed by
drb-audio-review (findings fixed; simultaneous two-channel mixdown filed
as #26) and drb-qa-review.
Not verified on SDR hardware.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
52 lines
1.9 KiB
Python
52 lines
1.9 KiB
Python
liquidsoap_config_template = """#!/usr/bin/liquidsoap
|
|
|
|
# OP25 → Icecast streaming (Liquidsoap 2.x)
|
|
|
|
settings.log.stdout.set(true)
|
|
settings.log.file.set(false)
|
|
settings.log.level.set(1)
|
|
settings.frame.audio.samplerate.set(8000)
|
|
settings.init.allow_root.set(true)
|
|
|
|
# ==========================================================
|
|
ICE_HOST = "${icecast_host}"
|
|
ICE_PORT = ${icecast_port}
|
|
ICE_MOUNT = "${icecast_mountpoint}"
|
|
ICE_PASSWORD = "${icecast_password}"
|
|
ICE_DESCRIPTION = "${icecast_description}"
|
|
ICE_GENRE = "${icecast_genre}"
|
|
# ==========================================================
|
|
|
|
input = mksafe(input.external(buffer=0.25, channels=2, samplerate=8000, restart_on_error=false, "./audio.py -x 2.5 -s${audio_py_args}"))
|
|
# Consider increasing the buffer value on slow systems such as RPi3. e.g. buffer=0.25
|
|
${two_channel_mix}
|
|
|
|
# Compression
|
|
input = compress(input, attack = 2.0, gain = 0.0, knee = 13.0, ratio = 2.0, release = 12.3, threshold = -18.0)
|
|
|
|
# Normalization
|
|
input = normalize(input, gain_max = 6.0, gain_min = -6.0, target = -16.0, threshold = -65.0)
|
|
|
|
# ==========================================================
|
|
# OUTPUTS
|
|
# ==========================================================
|
|
|
|
# Primary: PulseAudio null sink — used by edge-node for Discord streaming and call recording.
|
|
# The edge-node reads from the drb_sink.monitor source (set as PulseAudio default source).
|
|
output.pulseaudio(mean(input))
|
|
|
|
# Secondary: Icecast — for browser-based listening only (local node UI / server stream page).
|
|
# Do NOT use Icecast for recording or Discord; it introduces 2-5s buffering lag.
|
|
output.icecast(
|
|
%mp3(bitrate=16, samplerate=22050, stereo=false),
|
|
description=ICE_DESCRIPTION,
|
|
genre=ICE_GENRE,
|
|
url="",
|
|
fallible=false,
|
|
host=ICE_HOST,
|
|
port=ICE_PORT,
|
|
mount=ICE_MOUNT,
|
|
password=ICE_PASSWORD,
|
|
mean(input)
|
|
)
|
|
""" |