Pad the tgid_change closes so a talkgroup switch stops clipping the tail
The tgid_change and tgid_change_unlogged paths closed the outgoing segment with no tail pad, on the reasoning that the new grant's timestamp is an exact, already-known boundary. That is exact only in control-channel time. The buffered audio lags control timestamps by ~1.5s (measured 0.84-1.62s across seven field calls), so slicing there cut roughly the outgoing call's last 1.5s of speech - recordings ending mid-word with ~0s trailing silence. Both paths now pad, and the recorder's bounded tail wait blocks until that audio has actually been captured. TAIL_WAIT_TIMEOUT_SECONDS goes 2.0 -> 4.0 so it can satisfy the 3.0s pad instead of giving up and warning on every talkgroup switch. The incoming call's pre-roll is served from the ring buffer, so the delay costs it nothing. The two slices overlapping in the underlying audio is correct: the stream genuinely contains one call's tail and then the next call's start. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,11 @@ MAX_RECORDING_BYTES = MAX_RECORDING_SECONDS * _MP3_BYTES_PER_SECOND * 4
|
||||
# immediately therefore cuts the tail short — which costs the last word of the
|
||||
# transmission, usually the disposition or the address. Bounded so a dead capture
|
||||
# can never hang the upload path.
|
||||
TAIL_WAIT_TIMEOUT_SECONDS = 2.0
|
||||
#
|
||||
# Must exceed settings.call_tail_pad_seconds (default 3.0), otherwise a
|
||||
# tgid_change close — which pads past a timestamp that is still ~now — gives up
|
||||
# before the padded audio has been captured and warns on every talkgroup switch.
|
||||
TAIL_WAIT_TIMEOUT_SECONDS = 4.0
|
||||
TAIL_WAIT_POLL_SECONDS = 0.05
|
||||
|
||||
# Backoff bounds for restarting a dead capture process.
|
||||
|
||||
@@ -62,9 +62,20 @@ def _tail_pad() -> float:
|
||||
why the default moved 1.0 → 3.0 (short calls' recording window was closing
|
||||
before the ~1.5s grant→speech offset let voice audio even start).
|
||||
|
||||
Only the idle-timeout close path below uses this pad — the tgid_change and
|
||||
tgid_change_unlogged paths close at an exact, already-known boundary (the
|
||||
new grant's timestamp, or the same poll tick) and intentionally add none.
|
||||
All three close paths use this pad: idle_timeout, tgid_change, and
|
||||
tgid_change_unlogged. An earlier version of this docstring claimed the
|
||||
latter two close at "an exact, already-known boundary" (the new grant's
|
||||
timestamp, or the same poll tick) and so intentionally added no pad — THAT
|
||||
REASONING WAS WRONG and produced real truncated recordings. The boundary is
|
||||
exact only in CONTROL-CHANNEL time; the buffered AUDIO lags control-channel
|
||||
timestamps by ~1.5s (measured: 0.84-1.62s of lead trimmed across 7 field
|
||||
calls), so slicing the outgoing call at the new grant's exact timestamp cut
|
||||
roughly the last 1.5s of its real speech — calls ending mid-word with ~0s
|
||||
trailing silence. Do not reintroduce a zero-pad close for tgid_change or
|
||||
tgid_change_unlogged; if the outgoing and incoming recordings end up
|
||||
overlapping in the underlying audio because of this pad, that is correct —
|
||||
the audio genuinely contains both. See _handle_call_log and
|
||||
_handle_channels for how each path sources the timestamp this gets added to.
|
||||
"""
|
||||
return settings.call_tail_pad_seconds
|
||||
|
||||
@@ -194,9 +205,16 @@ class MetadataWatcher:
|
||||
return
|
||||
|
||||
# SPLIT: different talkgroup. The new grant's OP25 timestamp is the most
|
||||
# precise end available for the outgoing segment — the new call's audio
|
||||
# starts exactly there, so no tail pad.
|
||||
await self._close_segment(started_at, reason="tgid_change")
|
||||
# precise CONTROL-CHANNEL end for the outgoing segment, but the buffered
|
||||
# audio lags control by ~1.5s, so slicing exactly there cut the outgoing
|
||||
# call's last words. Pad past it and let the recorder's bounded tail wait
|
||||
# block until that audio has actually been captured.
|
||||
#
|
||||
# The incoming call's pre-roll comes from the ring buffer, so the delay
|
||||
# costs it nothing, and the two slices overlapping in the underlying
|
||||
# audio is correct — the stream genuinely contains one call's tail and
|
||||
# then the next call's start.
|
||||
await self._close_segment(started_at + _tail_pad(), reason="tgid_change")
|
||||
await self._open_segment(entry, tgid, started_at, now)
|
||||
|
||||
async def _handle_channels(self, channels: List[Dict[str, Any]], now: float) -> None:
|
||||
@@ -241,7 +259,7 @@ class MetadataWatcher:
|
||||
f"tgid {foreign_active_tgid} active without a call_log entry — "
|
||||
f"closing segment for tgid {self._current_tgid} (call_log event likely dropped)."
|
||||
)
|
||||
await self._close_segment(now, reason="tgid_change_unlogged")
|
||||
await self._close_segment(now + _tail_pad(), reason="tgid_change_unlogged")
|
||||
return
|
||||
|
||||
if (now - self._last_activity) >= settings.call_idle_timeout:
|
||||
|
||||
Reference in New Issue
Block a user