From 2e9bab75b14caddbd4a5a2af4f726d1962aad5cc Mon Sep 17 00:00:00 2001 From: Smittix Date: Thu, 19 Feb 2026 10:01:28 +0000 Subject: [PATCH] fix: Correct Scottie sync search to prevent decoder stall The previous sync search used search_margin = line_samples/10 (~306 samples for Scottie2), reaching deep into B channel pixel data behind pos and well past the expected sync end ahead of pos. When _find_sync returned a position in the late portion of that wide region, pos + R_channel_samples exceeded the buffer length. The buffer-too-short guard in _decode_line then returned early without consuming data or advancing the line counter, causing the stall guard in feed() to permanently break the decode loop. Fix: use a 50-sample backward margin (covers >130 ppm SDR drift) and a forward margin capped to whatever the current buffer can safely support for the R channel. A final candidate-position check before committing pos ensures no overflow is possible. Co-Authored-By: Claude Sonnet 4.6 --- utils/sstv/image_decoder.py | 44 +++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/utils/sstv/image_decoder.py b/utils/sstv/image_decoder.py index 33176a0..5d31205 100644 --- a/utils/sstv/image_decoder.py +++ b/utils/sstv/image_decoder.py @@ -235,20 +235,36 @@ class SSTVImageDecoder: pos += self._porch_samples else: # Scottie: sync + porch between B and R. - # Search for the actual sync pulse to correct for - # SDR clock drift — without this, any timing error - # accumulates line-by-line producing a visible slant. - search_margin = max(100, self._line_samples // 10) - sync_search_start = max(0, pos - search_margin) - sync_search_end = min( - len(self._buffer), - pos + self._sync_samples + search_margin, - ) - sync_region = self._buffer[sync_search_start:sync_search_end] - sync_found = self._find_sync(sync_region) - if sync_found is not None: - pos = (sync_search_start + sync_found - + self._sync_samples + self._porch_samples) + # Search for the actual sync pulse to correct per-line + # SDR clock drift — without this, timing errors + # accumulate line-by-line producing a visible slant. + # + # Constraints: + # - Backward margin is small (50 samples ≈ 4.5 ms) + # so we don't stray deep into B pixel data. + # - Forward margin is bounded by available buffer so + # the R channel decode never overflows the buffer. + # - The candidate position is validated before use. + r_samples = self._channel_samples[-1] + bwd = min(50, pos) + fwd = max(0, len(self._buffer) - pos + - self._sync_samples - self._porch_samples + - r_samples) + fwd = min(fwd, self._sync_samples) + if bwd + fwd > 0: + sync_region = self._buffer[ + pos - bwd: pos + self._sync_samples + fwd] + sync_found = self._find_sync(sync_region) + if sync_found is not None: + candidate = (pos - bwd + sync_found + + self._sync_samples + + self._porch_samples) + if candidate + r_samples <= len(self._buffer): + pos = candidate + else: + pos += self._sync_samples + self._porch_samples + else: + pos += self._sync_samples + self._porch_samples else: pos += self._sync_samples + self._porch_samples elif self._separator_samples > 0: