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
|
# 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
|
# transmission, usually the disposition or the address. Bounded so a dead capture
|
||||||
# can never hang the upload path.
|
# 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
|
TAIL_WAIT_POLL_SECONDS = 0.05
|
||||||
|
|
||||||
# Backoff bounds for restarting a dead capture process.
|
# 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
|
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).
|
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
|
All three close paths use this pad: idle_timeout, tgid_change, and
|
||||||
tgid_change_unlogged paths close at an exact, already-known boundary (the
|
tgid_change_unlogged. An earlier version of this docstring claimed the
|
||||||
new grant's timestamp, or the same poll tick) and intentionally add none.
|
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
|
return settings.call_tail_pad_seconds
|
||||||
|
|
||||||
@@ -194,9 +205,16 @@ class MetadataWatcher:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# SPLIT: different talkgroup. The new grant's OP25 timestamp is the most
|
# SPLIT: different talkgroup. The new grant's OP25 timestamp is the most
|
||||||
# precise end available for the outgoing segment — the new call's audio
|
# precise CONTROL-CHANNEL end for the outgoing segment, but the buffered
|
||||||
# starts exactly there, so no tail pad.
|
# audio lags control by ~1.5s, so slicing exactly there cut the outgoing
|
||||||
await self._close_segment(started_at, reason="tgid_change")
|
# 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)
|
await self._open_segment(entry, tgid, started_at, now)
|
||||||
|
|
||||||
async def _handle_channels(self, channels: List[Dict[str, Any]], now: float) -> None:
|
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"tgid {foreign_active_tgid} active without a call_log entry — "
|
||||||
f"closing segment for tgid {self._current_tgid} (call_log event likely dropped)."
|
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
|
return
|
||||||
|
|
||||||
if (now - self._last_activity) >= settings.call_idle_timeout:
|
if (now - self._last_activity) >= settings.call_idle_timeout:
|
||||||
|
|||||||
Reference in New Issue
Block a user