mirror of
https://github.com/smittix/intercept.git
synced 2026-07-18 22:38:10 -07:00
Fix shared waterfall monitor tuning across in-span clicks
This commit is contained in:
@@ -212,18 +212,21 @@ def _demodulate_monitor_audio(
|
|||||||
monitor_freq_mhz: float,
|
monitor_freq_mhz: float,
|
||||||
modulation: str,
|
modulation: str,
|
||||||
squelch: int,
|
squelch: int,
|
||||||
) -> bytes | None:
|
rotator_phase: float = 0.0,
|
||||||
|
) -> tuple[bytes | None, float]:
|
||||||
if samples.size < 32 or sample_rate <= 0:
|
if samples.size < 32 or sample_rate <= 0:
|
||||||
return None
|
return None, float(rotator_phase)
|
||||||
|
|
||||||
fs = float(sample_rate)
|
fs = float(sample_rate)
|
||||||
freq_offset_hz = (float(monitor_freq_mhz) - float(center_mhz)) * 1e6
|
freq_offset_hz = (float(monitor_freq_mhz) - float(center_mhz)) * 1e6
|
||||||
nyquist = fs * 0.5
|
nyquist = fs * 0.5
|
||||||
if abs(freq_offset_hz) > nyquist * 0.98:
|
if abs(freq_offset_hz) > nyquist * 0.98:
|
||||||
return None
|
return None, float(rotator_phase)
|
||||||
|
|
||||||
n = np.arange(samples.size, dtype=np.float32)
|
phase_inc = (2.0 * np.pi * freq_offset_hz) / fs
|
||||||
rotator = np.exp(-1j * (2.0 * np.pi * freq_offset_hz / fs) * n)
|
n = np.arange(samples.size, dtype=np.float64)
|
||||||
|
rotator = np.exp(-1j * (float(rotator_phase) + phase_inc * n)).astype(np.complex64)
|
||||||
|
next_phase = float((float(rotator_phase) + phase_inc * samples.size) % (2.0 * np.pi))
|
||||||
shifted = samples * rotator
|
shifted = samples * rotator
|
||||||
|
|
||||||
mod = str(modulation or 'wfm').lower().strip()
|
mod = str(modulation or 'wfm').lower().strip()
|
||||||
@@ -232,11 +235,11 @@ def _demodulate_monitor_audio(
|
|||||||
if pre_decim > 1:
|
if pre_decim > 1:
|
||||||
usable = (shifted.size // pre_decim) * pre_decim
|
usable = (shifted.size // pre_decim) * pre_decim
|
||||||
if usable < pre_decim:
|
if usable < pre_decim:
|
||||||
return None
|
return None, next_phase
|
||||||
shifted = shifted[:usable].reshape(-1, pre_decim).mean(axis=1)
|
shifted = shifted[:usable].reshape(-1, pre_decim).mean(axis=1)
|
||||||
fs1 = fs / pre_decim
|
fs1 = fs / pre_decim
|
||||||
if shifted.size < 16:
|
if shifted.size < 16:
|
||||||
return None
|
return None, next_phase
|
||||||
|
|
||||||
if mod in ('wfm', 'fm'):
|
if mod in ('wfm', 'fm'):
|
||||||
audio = np.angle(shifted[1:] * np.conj(shifted[:-1])).astype(np.float32)
|
audio = np.angle(shifted[1:] * np.conj(shifted[:-1])).astype(np.float32)
|
||||||
@@ -251,7 +254,7 @@ def _demodulate_monitor_audio(
|
|||||||
audio = np.real(shifted).astype(np.float32)
|
audio = np.real(shifted).astype(np.float32)
|
||||||
|
|
||||||
if audio.size < 8:
|
if audio.size < 8:
|
||||||
return None
|
return None, next_phase
|
||||||
|
|
||||||
audio = audio - float(np.mean(audio))
|
audio = audio - float(np.mean(audio))
|
||||||
|
|
||||||
@@ -263,7 +266,7 @@ def _demodulate_monitor_audio(
|
|||||||
|
|
||||||
out_len = int(audio.size * AUDIO_SAMPLE_RATE / fs1)
|
out_len = int(audio.size * AUDIO_SAMPLE_RATE / fs1)
|
||||||
if out_len < 32:
|
if out_len < 32:
|
||||||
return None
|
return None, next_phase
|
||||||
x_old = np.linspace(0.0, 1.0, audio.size, endpoint=False, dtype=np.float32)
|
x_old = np.linspace(0.0, 1.0, audio.size, endpoint=False, dtype=np.float32)
|
||||||
x_new = np.linspace(0.0, 1.0, out_len, endpoint=False, dtype=np.float32)
|
x_new = np.linspace(0.0, 1.0, out_len, endpoint=False, dtype=np.float32)
|
||||||
audio = np.interp(x_new, x_old, audio).astype(np.float32)
|
audio = np.interp(x_new, x_old, audio).astype(np.float32)
|
||||||
@@ -278,7 +281,7 @@ def _demodulate_monitor_audio(
|
|||||||
audio = audio * min(20.0, 0.85 / peak)
|
audio = audio * min(20.0, 0.85 / peak)
|
||||||
|
|
||||||
pcm = np.clip(audio, -1.0, 1.0)
|
pcm = np.clip(audio, -1.0, 1.0)
|
||||||
return (pcm * 32767.0).astype(np.int16).tobytes()
|
return (pcm * 32767.0).astype(np.int16).tobytes(), next_phase
|
||||||
|
|
||||||
|
|
||||||
def _parse_center_freq_mhz(payload: dict[str, Any]) -> float:
|
def _parse_center_freq_mhz(payload: dict[str, Any]) -> float:
|
||||||
@@ -620,6 +623,8 @@ def init_waterfall_websocket(app: Flask):
|
|||||||
timeslice_samples = max(required_fft_samples, int(_sample_rate / max(1, _fps)))
|
timeslice_samples = max(required_fft_samples, int(_sample_rate / max(1, _fps)))
|
||||||
bytes_per_frame = timeslice_samples * 2
|
bytes_per_frame = timeslice_samples * 2
|
||||||
frame_interval = 1.0 / _fps
|
frame_interval = 1.0 / _fps
|
||||||
|
monitor_rotator_phase = 0.0
|
||||||
|
last_monitor_offset_hz = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while not stop_evt.is_set():
|
while not stop_evt.is_set():
|
||||||
@@ -664,16 +669,30 @@ def init_waterfall_websocket(app: Flask):
|
|||||||
|
|
||||||
monitor_cfg = _snapshot_monitor_config()
|
monitor_cfg = _snapshot_monitor_config()
|
||||||
if monitor_cfg:
|
if monitor_cfg:
|
||||||
audio_chunk = _demodulate_monitor_audio(
|
center_mhz_cfg = float(monitor_cfg.get('center_mhz', _center_mhz))
|
||||||
|
monitor_mhz_cfg = float(monitor_cfg.get('monitor_freq_mhz', _center_mhz))
|
||||||
|
offset_hz = (monitor_mhz_cfg - center_mhz_cfg) * 1e6
|
||||||
|
if (
|
||||||
|
last_monitor_offset_hz is None
|
||||||
|
or abs(offset_hz - last_monitor_offset_hz) > 1.0
|
||||||
|
):
|
||||||
|
monitor_rotator_phase = 0.0
|
||||||
|
last_monitor_offset_hz = offset_hz
|
||||||
|
|
||||||
|
audio_chunk, monitor_rotator_phase = _demodulate_monitor_audio(
|
||||||
samples=samples,
|
samples=samples,
|
||||||
sample_rate=_sample_rate,
|
sample_rate=_sample_rate,
|
||||||
center_mhz=monitor_cfg.get('center_mhz', _center_mhz),
|
center_mhz=center_mhz_cfg,
|
||||||
monitor_freq_mhz=monitor_cfg.get('monitor_freq_mhz', _center_mhz),
|
monitor_freq_mhz=monitor_mhz_cfg,
|
||||||
modulation=monitor_cfg.get('modulation', 'wfm'),
|
modulation=monitor_cfg.get('modulation', 'wfm'),
|
||||||
squelch=int(monitor_cfg.get('squelch', 0)),
|
squelch=int(monitor_cfg.get('squelch', 0)),
|
||||||
|
rotator_phase=monitor_rotator_phase,
|
||||||
)
|
)
|
||||||
if audio_chunk:
|
if audio_chunk:
|
||||||
_push_shared_audio_chunk(audio_chunk)
|
_push_shared_audio_chunk(audio_chunk)
|
||||||
|
else:
|
||||||
|
monitor_rotator_phase = 0.0
|
||||||
|
last_monitor_offset_hz = None
|
||||||
|
|
||||||
# Pace to target FPS
|
# Pace to target FPS
|
||||||
elapsed = time.monotonic() - frame_start
|
elapsed = time.monotonic() - frame_start
|
||||||
|
|||||||
Reference in New Issue
Block a user