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 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-19 10:01:28 +00:00
parent 0dc40bbea3
commit 2e9bab75b1
+30 -14
View File
@@ -235,20 +235,36 @@ class SSTVImageDecoder:
pos += self._porch_samples pos += self._porch_samples
else: else:
# Scottie: sync + porch between B and R. # Scottie: sync + porch between B and R.
# Search for the actual sync pulse to correct for # Search for the actual sync pulse to correct per-line
# SDR clock drift — without this, any timing error # SDR clock drift — without this, timing errors
# accumulates line-by-line producing a visible slant. # accumulate line-by-line producing a visible slant.
search_margin = max(100, self._line_samples // 10) #
sync_search_start = max(0, pos - search_margin) # Constraints:
sync_search_end = min( # - Backward margin is small (50 samples ≈ 4.5 ms)
len(self._buffer), # so we don't stray deep into B pixel data.
pos + self._sync_samples + search_margin, # - Forward margin is bounded by available buffer so
) # the R channel decode never overflows the buffer.
sync_region = self._buffer[sync_search_start:sync_search_end] # - The candidate position is validated before use.
sync_found = self._find_sync(sync_region) r_samples = self._channel_samples[-1]
if sync_found is not None: bwd = min(50, pos)
pos = (sync_search_start + sync_found fwd = max(0, len(self._buffer) - pos
+ self._sync_samples + self._porch_samples) - 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: else:
pos += self._sync_samples + self._porch_samples pos += self._sync_samples + self._porch_samples
elif self._separator_samples > 0: elif self._separator_samples > 0: