Tune mumble bridge to prevent latency drift

This commit is contained in:
Kai
2026-02-15 16:10:26 +01:00
parent 6f60e05c5d
commit 3f24033cb1
2 changed files with 34 additions and 2 deletions

View File

@@ -45,6 +45,8 @@ Deploy one container per EVS client so every device appears as its own Mumble us
- `VAD_CLOSE_FRAMES`: consecutive silence frames to close gate (default `20`) - `VAD_CLOSE_FRAMES`: consecutive silence frames to close gate (default `20`)
- `HPF_ENABLED`: enable high-pass/DC blocker to reduce low-frequency rumble (default `true`) - `HPF_ENABLED`: enable high-pass/DC blocker to reduce low-frequency rumble (default `true`)
- `HPF_CUTOFF_HZ`: high-pass cutoff frequency (default `120.0`) - `HPF_CUTOFF_HZ`: high-pass cutoff frequency (default `120.0`)
- `TX_BUFFER_MAX_MS`: maximum bridge TX buffer before dropping old audio to stop latency growth (default `800`)
- `TX_BUFFER_TARGET_MS`: buffer target after drop (default `300`)
## Example docker compose service ## Example docker compose service

View File

@@ -58,6 +58,8 @@ VAD_OPEN_FRAMES = int(os.getenv("VAD_OPEN_FRAMES", "2"))
VAD_CLOSE_FRAMES = int(os.getenv("VAD_CLOSE_FRAMES", "20")) VAD_CLOSE_FRAMES = int(os.getenv("VAD_CLOSE_FRAMES", "20"))
HPF_ENABLED = getenv_bool("HPF_ENABLED", True) HPF_ENABLED = getenv_bool("HPF_ENABLED", True)
HPF_CUTOFF_HZ = float(os.getenv("HPF_CUTOFF_HZ", "120.0")) HPF_CUTOFF_HZ = float(os.getenv("HPF_CUTOFF_HZ", "120.0"))
TX_BUFFER_MAX_MS = int(os.getenv("TX_BUFFER_MAX_MS", "800"))
TX_BUFFER_TARGET_MS = int(os.getenv("TX_BUFFER_TARGET_MS", "300"))
def _channel_name(ch) -> str: def _channel_name(ch) -> str:
@@ -456,10 +458,14 @@ def run() -> None:
hp_prev_y = 0.0 hp_prev_y = 0.0
out_buffer = bytearray() out_buffer = bytearray()
frame_bytes = int((MUMBLE_SAMPLE_RATE * 2 * FRAME_MS) / 1000) # mono, 16-bit frame_bytes = int((MUMBLE_SAMPLE_RATE * 2 * FRAME_MS) / 1000) # mono, 16-bit
frame_period_s = FRAME_MS / 1000.0
next_send_ts = time.monotonic()
udp_packets = 0 udp_packets = 0
udp_bytes = 0 udp_bytes = 0
frames_sent = 0 frames_sent = 0
send_errors = 0 send_errors = 0
dropped_bytes = 0
dropped_frames = 0
vad_dropped_packets = 0 vad_dropped_packets = 0
vad_open = not VAD_ENABLED vad_open = not VAD_ENABLED
vad_voice_frames = 0 vad_voice_frames = 0
@@ -476,6 +482,7 @@ def run() -> None:
rate_state = None rate_state = None
hp_prev_x = 0.0 hp_prev_x = 0.0
hp_prev_y = 0.0 hp_prev_y = 0.0
next_send_ts = time.monotonic()
except Exception: except Exception:
log.exception("mumble connect failed, retrying in %ss", MUMBLE_RECONNECT_SEC) log.exception("mumble connect failed, retrying in %ss", MUMBLE_RECONNECT_SEC)
time.sleep(MUMBLE_RECONNECT_SEC) time.sleep(MUMBLE_RECONNECT_SEC)
@@ -494,13 +501,15 @@ def run() -> None:
if BRIDGE_STATS_INTERVAL_SEC > 0 and (now - stats_t0) >= BRIDGE_STATS_INTERVAL_SEC: if BRIDGE_STATS_INTERVAL_SEC > 0 and (now - stats_t0) >= BRIDGE_STATS_INTERVAL_SEC:
dt = max(0.001, now - stats_t0) dt = max(0.001, now - stats_t0)
log.info( log.info(
"bridge stats: udp_packets=%d udp_bytes=%d udp_kbps=%.1f frames_sent=%d send_errors=%d buffer_bytes=%d vad_open=%s vad_dropped=%d last_from=%s", "bridge stats: udp_packets=%d udp_bytes=%d udp_kbps=%.1f frames_sent=%d send_errors=%d buffer_bytes=%d dropped_frames=%d dropped_bytes=%d vad_open=%s vad_dropped=%d last_from=%s",
udp_packets, udp_packets,
udp_bytes, udp_bytes,
(udp_bytes * 8.0 / 1000.0) / dt, (udp_bytes * 8.0 / 1000.0) / dt,
frames_sent, frames_sent,
send_errors, send_errors,
len(out_buffer), len(out_buffer),
dropped_frames,
dropped_bytes,
vad_open, vad_open,
vad_dropped_packets, vad_dropped_packets,
last_udp_from or "-", last_udp_from or "-",
@@ -509,6 +518,8 @@ def run() -> None:
udp_bytes = 0 udp_bytes = 0
frames_sent = 0 frames_sent = 0
send_errors = 0 send_errors = 0
dropped_bytes = 0
dropped_frames = 0
vad_dropped_packets = 0 vad_dropped_packets = 0
stats_t0 = now stats_t0 = now
@@ -550,11 +561,30 @@ def run() -> None:
) )
out_buffer.extend(processed) out_buffer.extend(processed)
while len(out_buffer) >= frame_bytes: if TX_BUFFER_MAX_MS > 0:
max_bytes = int((MUMBLE_SAMPLE_RATE * 2 * TX_BUFFER_MAX_MS) / 1000)
target_bytes = int((MUMBLE_SAMPLE_RATE * 2 * TX_BUFFER_TARGET_MS) / 1000)
if target_bytes < frame_bytes:
target_bytes = frame_bytes
if len(out_buffer) > max_bytes:
to_drop = len(out_buffer) - target_bytes
# Keep alignment at 16-bit boundaries.
to_drop -= to_drop % 2
if to_drop > 0:
del out_buffer[:to_drop]
dropped_bytes += to_drop
dropped_frames += to_drop // frame_bytes
while len(out_buffer) >= frame_bytes and time.monotonic() >= next_send_ts:
frame = bytes(out_buffer[:frame_bytes]) frame = bytes(out_buffer[:frame_bytes])
del out_buffer[:frame_bytes] del out_buffer[:frame_bytes]
mumble.sound_output.add_sound(frame) mumble.sound_output.add_sound(frame)
frames_sent += 1 frames_sent += 1
next_send_ts += frame_period_s
# Prevent "runaway catchup" if scheduler was stalled.
now_mono = time.monotonic()
if (now_mono - next_send_ts) > (frame_period_s * 5):
next_send_ts = now_mono
except Exception: except Exception:
send_errors += 1 send_errors += 1
log.exception("audio processing/send failed") log.exception("audio processing/send failed")