#!/usr/bin/python3 import numpy as np import scipy.io.wavfile as wav from scipy.signal import hilbert def load_and_demodulate(wav_path): """Lädt die WAV-Datei und berechnet die Hüllkurve des Signals.""" sample_rate, data = wav.read(wav_path) # Falls Stereo, in Mono umwandeln if len(data.shape) > 1: data = data[:, 0] # Normalisieren data = data.astype(np.float32) / np.max(np.abs(data)) # Hüllkurve mittels Analytischem Signal (Hilbert-Transformation) bestimmen # Falls das Signal bereits ein reines AM-Demodulations-Audio ist, reicht np.abs(data) envelope = np.abs(hilbert(data)) # Tiefpass/Glättung über ein gleitendes Fenster (z.B. 10ms) window_len = int(sample_rate * 0.01) envelope_smooth = np.convolve(envelope, np.ones(window_len)/window_len, mode='same') return sample_rate, envelope_smooth def binarize_signal(envelope, sample_rate): """Wandelt die Hüllkurve über einen adaptiven Schwellenwert in High/Low um.""" # MSF schaltet auf 0% ab. Ein Schwellenwert bei 35% des Maximums ist meist robust gegen Rauschen. threshold = np.percentile(envelope, 35) binary = (envelope > threshold).astype(np.int8) return binary def extract_pulses(binary, sample_rate): """Findet alle Flankenwechsel und berechnet die Pulsdauern in Millisekunden.""" # Wechsel finden (1 zu 0 oder 0 zu 1) diff = np.diff(binary) edges = np.where(diff != 0)[0] pulses = [] # Wir messen die Dauer der Absenkungen (Träger AUS = 0) for i in range(len(edges) - 1): idx = edges[i] # Wenn das Signal auf 0 abfällt if binary[idx] == 1 and binary[idx+1] == 0: duration_ms = (edges[i+1] - idx) / sample_rate * 1000 start_ms = idx / sample_rate * 1000 pulses.append((start_ms, duration_ms)) return pulses def decode_msf(pulses): """Parst die Pulsliste basierend auf dem MSF-Timing.""" print(f"Starte Dekodierung von {len(pulses)} erkannten Pulsen...\n") current_minute = [] in_sync = False # Hilfsfunktion zum Zuordnen der Pulsdauer mit Toleranz def get_pulse_type(dur): if 70 <= dur <= 140: return 100 # 100 ms Bit if 170 <= dur <= 240: return 200 # 200 ms Bit if 450 <= dur <= 550: return 500 # 500 ms Minutenmarker return None for i in range(len(pulses) - 1): start, duration = pulses[i] p_type = get_pulse_type(duration) if p_type == 500: print(f"\n--- [Minutenstart erkannt bei {start/1000:.2f}s] ---") if in_sync and len(current_minute) > 0: # Hier könnte man die gesammelten Bits der Vor-Minute auswerten print(f"Bits der letzten Minute: {current_minute}") current_minute = [] in_sync = True continue if in_sync and p_type is not None: # Zeitabstand zum nächsten Puls berechnen, um Bit A und B zu trennen next_start = pulses[i+1][0] gap = next_start - start # Wenn der nächste Puls innerhalb der gleichen Sekunde liegt (~300ms Abstand) # handelt es sich um eine Bit-A / Bit-B Sequenz. if 250 <= gap <= 350: p_type_b = get_pulse_type(pulses[i+1][1]) bit_a = 1 if p_type == 200 else 0 bit_b = 1 if p_type_b == 100 else 0 current_minute.append((bit_a, bit_b)) # Überspringe den nächsten Puls, da als Bit B verarbeitet i += 1 else: # Nur Bit A vorhanden (kein zweiter Puls bei 300ms) bit_a = 1 if p_type == 200 else 0 current_minute.append((bit_a, 0)) # --- Hauptprogramm --- if __name__ == "__main__": # Pfad zu deiner vom WebSDR heruntergeladenen WAV-Datei eintragen wav_filename = "websdr_recording_60khz.wav" try: sr, env = load_and_demodulate(wav_filename) bin_sig = binarize_signal(env, sr) detected_pulses = extract_pulses(bin_sig, sr) decode_msf(detected_pulses) except FileNotFoundError: print(f"Datei {wav_filename} nicht gefunden. Bitte Pfad anpassen.")