#!/usr/bin/python3 import sys import numpy as np from scipy.io import wavfile from scipy.signal import hilbert def decode_msf_frame(frame_bits): """ Decodiert einen 60-Sekunden-Rahmen von Bit A Daten gemäß NPL-MSF Spezifikation. """ try: # BCD-Gewichtungen für MSF (NPL Spec): # Jahr: 17-20 (80,40,20,10), 21-24 (8,4,2,1) year_tens = sum(b * w for b, w in zip(frame_bits[17:21], [8, 4, 2, 1])) year_ones = sum(b * w for b, w in zip(frame_bits[21:25], [8, 4, 2, 1])) year = year_tens * 10 + year_ones # Monat: 25 (10), 26-29 (8,4,2,1) month_tens = frame_bits[25] * 1 month_ones = sum(b * w for b, w in zip(frame_bits[26:30], [8, 4, 2, 1])) month = month_tens * 10 + month_ones # Tag: 30-31 (20,10), 32-35 (8,4,2,1) day_tens = sum(b * w for b, w in zip(frame_bits[30:32], [2, 1])) day_ones = sum(b * w for b, w in zip(frame_bits[32:36], [8, 4, 2, 1])) day = day_tens * 10 + day_ones # Wochentag: 36-38 (4,2,1) -> 0=So, 1=Mo ... 6=Sa weekday_num = sum(b * w for b, w in zip(frame_bits[36:39], [4, 2, 1])) # Stunde: 39-40 (20,10), 41-44 (8,4,2,1) hour_tens = sum(b * w for b, w in zip(frame_bits[39:41], [2, 1])) hour_ones = sum(b * w for b, w in zip(frame_bits[41:45], [8, 4, 2, 1])) hour = hour_tens * 10 + hour_ones # Minute: 45-47 (40,20,10), 48-51 (8,4,2,1) min_tens = sum(b * w for b, w in zip(frame_bits[45:48], [4, 2, 1])) min_ones = sum(b * w for b, w in zip(frame_bits[48:52], [8, 4, 2, 1])) minute = min_tens * 10 + min_ones # Sommerzeit (BST): Sekunde 53 (1 = aktiv) is_bst = frame_bits[53] == 1 if len(frame_bits) > 53 else False days = ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"] day_str = days[weekday_num] if weekday_num < len(days) else f"Tag {weekday_num}" tz_str = "BST (UTC+1)" if is_bst else "UTC" return f"{day_str}, {day:02d}.{month:02d}.20{year:02d} - {hour:02d}:{minute:02d}:00 {tz_str}" except Exception as e: return f"Fehler beim Parsen: {e}" def process_wav(wav_pfad): print(f"Lese Audio-Datei: {wav_pfad}...") samplerate, data = wavfile.read(wav_pfad) if len(data.shape) > 1: data = data[:, 0] # Normalisieren data = data.astype(np.float32) data /= np.max(np.abs(data)) if np.max(np.abs(data)) != 0 else 1.0 # Hüllkurve envelope = np.abs(hilbert(data)) window_len = int(samplerate * 0.01) # 10 ms Glättung envelope = np.convolve(envelope, np.ones(window_len)/window_len, mode='same') # Schwellenwert für Absenkungen base_level = np.median(envelope) threshold = base_level * 0.6 binary = (envelope < threshold).astype(np.int8) # Flanken diff = np.diff(binary) rising = np.where(diff == 1)[0] falling = np.where(diff == -1)[0] pulses = [] for r in rising: f_candidates = falling[falling > r] if len(f_candidates) > 0: f = f_candidates[0] duration_ms = (f - r) / samplerate * 1000.0 start_sec = r / samplerate if duration_ms >= 40: # Glitches unter 40ms filtern pulses.append((start_sec, duration_ms)) # Minuten-Marker suchen (~500ms Absenkung) minute_markers = [] last_marker_time = -100 for start_sec, dur in pulses: if 400 <= dur <= 600: if (start_sec - last_marker_time) > 45.0: minute_markers.append(float(start_sec)) last_marker_time = start_sec print(f"\n{len(pulses)} Absenkungs-Impulse gefunden.") print(f"{len(minute_markers)} valide Minutenmarker (500 ms Absenkung) bei: {[round(m, 1) for m in minute_markers]} s\n") if not minute_markers: print("Kein gültiger Minutenmarker gefunden.") return print("=" * 60) print(" DEKODIERTE MSF ZEIT- UND DATUMSANZEIGE") print("=" * 60) for m_idx, marker_start in enumerate(minute_markers): frame_bits = [0] * 60 next_marker = minute_markers[m_idx+1] if m_idx+1 < len(minute_markers) else marker_start + 60.0 for start_sec, dur in pulses: if marker_start <= start_sec < next_marker: # Exaktes Sekunden-Raster berechnen rel_time = start_sec - marker_start rel_sec = int(round(rel_time)) sub_sec_offset = rel_time - rel_sec # Nur Bit A auswerten (Impuls direkt am Sekundenanfang, Offset < 200ms) if 0 <= rel_sec < 60 and abs(sub_sec_offset) < 0.2: if 140 <= dur <= 280: # 200 ms Absenkung -> Bit A = 1 frame_bits[rel_sec] = 1 elif 60 <= dur < 140: # 100 ms Absenkung -> Bit A = 0 frame_bits[rel_sec] = 0 zeit_text = decode_msf_frame(frame_bits) print(f"[@ {marker_start:6.1f}s Audio-Offset] -> {zeit_text}") print("=" * 60) if __name__ == "__main__": datei = sys.argv[1] if len(sys.argv) > 1 else "websdr_recording_60khz.wav" process_wav(datei)