9x25dillon commited on
Commit
3efa90c
·
verified ·
1 Parent(s): f58cae6

Create ASPM_system.py

Browse files
Files changed (1) hide show
  1. ASPM_system.py +898 -0
ASPM_system.py ADDED
@@ -0,0 +1,898 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/Aur/bin/env python3
2
+ """
3
+ Advanced Signal Processing and Modulation System
4
+ ===============================================
5
+
6
+ This module implements comprehensive digital signal processing including:
7
+ - Multiple modulation schemes (BFSK, BPSK, QPSK, QAM16, OFDM, DSSS)
8
+ - Forward Error Correction (FEC) coding
9
+ - Framing, security, and watermarking
10
+ - Audio and IQ signal generation
11
+ - Visualization and analysis tools
12
+
13
+ Author: Assistant
14
+ License: MIT
15
+ """
16
+
17
+ import binascii
18
+ import hashlib
19
+ import math
20
+ import struct
21
+ import time
22
+ import wave
23
+ from dataclasses import dataclass
24
+ from enum import Enum, auto
25
+ from pathlib import Path
26
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
27
+
28
+ import numpy as np
29
+ from scipy import signal as sp_signal
30
+ from scipy.fft import rfft, rfftfreq
31
+
32
+ try:
33
+ import matplotlib.pyplot as plt
34
+ HAS_MATPLOTLIB = True
35
+ except ImportError:
36
+ HAS_MATPLOTLIB = False
37
+
38
+ try:
39
+ import sounddevice as sd
40
+ HAS_AUDIO = True
41
+ except ImportError:
42
+ HAS_AUDIO = False
43
+
44
+ try:
45
+ from Crypto.Cipher import AES
46
+ from Crypto.Random import get_random_bytes
47
+ from Crypto.Protocol.KDF import PBKDF2
48
+ HAS_CRYPTO = True
49
+ except ImportError:
50
+ HAS_CRYPTO = False
51
+
52
+ import logging
53
+
54
+ logging.basicConfig(level=logging.INFO)
55
+ logger = logging.getLogger(__name__)
56
+
57
+ # =========================================================
58
+ # Enums and Configuration
59
+ # =========================================================
60
+
61
+ class ModulationScheme(Enum):
62
+ BFSK = auto()
63
+ BPSK = auto()
64
+ QPSK = auto()
65
+ QAM16 = auto()
66
+ AFSK = auto()
67
+ OFDM = auto()
68
+ DSSS_BPSK = auto()
69
+
70
+ class FEC(Enum):
71
+ NONE = auto()
72
+ HAMMING74 = auto()
73
+ REED_SOLOMON = auto() # stub
74
+ LDPC = auto() # stub
75
+ TURBO = auto() # stub
76
+
77
+ @dataclass
78
+ class ModConfig:
79
+ sample_rate: int = 48000
80
+ symbol_rate: int = 1200
81
+ amplitude: float = 0.7
82
+ f0: float = 1200.0 # BFSK 0
83
+ f1: float = 2200.0 # BFSK 1
84
+ fc: float = 1800.0 # PSK/QAM audio carrier (for WAV)
85
+ clip: bool = True
86
+ # OFDM parameters
87
+ ofdm_subc: int = 64
88
+ cp_len: int = 16
89
+ # DSSS parameters
90
+ dsss_chip_rate: int = 4800
91
+
92
+ @dataclass
93
+ class FrameConfig:
94
+ use_crc32: bool = True
95
+ use_crc16: bool = False
96
+ preamble: bytes = b"\x55" * 8 # 01010101 * 8
97
+ version: int = 1
98
+
99
+ @dataclass
100
+ class SecurityConfig:
101
+ password: Optional[str] = None # AES-GCM if provided
102
+ watermark: Optional[str] = None # prepended SHA256[0:8]
103
+ hmac_key: Optional[str] = None # HMAC-SHA256 appended
104
+
105
+ @dataclass
106
+ class OutputPaths:
107
+ wav: Optional[Path] = None
108
+ iq: Optional[Path] = None
109
+ meta: Optional[Path] = None
110
+ png: Optional[Path] = None
111
+
112
+ # =========================================================
113
+ # Utility Functions
114
+ # =========================================================
115
+
116
+ def now_ms() -> int:
117
+ return int(time.time() * 1000)
118
+
119
+ def crc32_bytes(data: bytes) -> bytes:
120
+ return binascii.crc32(data).to_bytes(4, "big")
121
+
122
+ def crc16_ccitt(data: bytes) -> bytes:
123
+ poly, crc = 0x1021, 0xFFFF
124
+ for b in data:
125
+ crc ^= b << 8
126
+ for _ in range(8):
127
+ crc = ((crc << 1) ^ poly) & 0xFFFF if (crc & 0x8000) else ((crc << 1) & 0xFFFF)
128
+ return crc.to_bytes(2, "big")
129
+
130
+ def to_bits(data: bytes) -> List[int]:
131
+ return [(byte >> i) & 1 for byte in data for i in range(7, -1, -1)]
132
+
133
+ def from_bits(bits: Sequence[int]) -> bytes:
134
+ if len(bits) % 8 != 0:
135
+ bits = list(bits) + [0] * (8 - len(bits) % 8)
136
+ out = bytearray()
137
+ for i in range(0, len(bits), 8):
138
+ byte = 0
139
+ for b in bits[i:i+8]:
140
+ byte = (byte << 1) | (1 if b else 0)
141
+ out.append(byte)
142
+ return bytes(out)
143
+
144
+ def chunk_bits(bits: Sequence[int], n: int) -> List[List[int]]:
145
+ return [list(bits[i:i+n]) for i in range(0, len(bits), n)]
146
+
147
+ def safe_json(obj: Any) -> str:
148
+ import json
149
+ def enc(x):
150
+ if isinstance(x, (np.floating,)):
151
+ return float(x)
152
+ if isinstance(x, (np.integer,)):
153
+ return int(x)
154
+ if isinstance(x, (np.ndarray,)):
155
+ return x.tolist()
156
+ if isinstance(x, complex):
157
+ return {"real": float(x.real), "imag": float(x.imag)}
158
+ return str(x)
159
+ return json.dumps(obj, ensure_ascii=False, indent=2, default=enc)
160
+
161
+ # =========================================================
162
+ # FEC Implementation
163
+ # =========================================================
164
+
165
+ def hamming74_encode(data_bits: List[int]) -> List[int]:
166
+ """Hamming (7,4) encoding"""
167
+ if len(data_bits) % 4 != 0:
168
+ data_bits = data_bits + [0] * (4 - len(data_bits) % 4)
169
+
170
+ out = []
171
+ for i in range(0, len(data_bits), 4):
172
+ d0, d1, d2, d3 = data_bits[i:i+4]
173
+ p1 = d0 ^ d1 ^ d3
174
+ p2 = d0 ^ d2 ^ d3
175
+ p3 = d1 ^ d2 ^ d3
176
+ out += [p1, p2, d0, p3, d1, d2, d3]
177
+
178
+ return out
179
+
180
+ def hamming74_decode(coded_bits: List[int]) -> Tuple[List[int], int]:
181
+ """Hamming (7,4) decoding with error correction"""
182
+ if len(coded_bits) % 7 != 0:
183
+ coded_bits = coded_bits + [0] * (7 - len(coded_bits) % 7)
184
+
185
+ decoded = []
186
+ errors_corrected = 0
187
+
188
+ for i in range(0, len(coded_bits), 7):
189
+ r = coded_bits[i:i+7] # received codeword
190
+ p1, p2, d0, p3, d1, d2, d3 = r
191
+
192
+ # Calculate syndrome
193
+ s1 = p1 ^ d0 ^ d1 ^ d3
194
+ s2 = p2 ^ d0 ^ d2 ^ d3
195
+ s3 = p3 ^ d1 ^ d2 ^ d3
196
+
197
+ syndrome = s1 + 2*s2 + 4*s3
198
+
199
+ # Correct single-bit errors
200
+ if syndrome != 0:
201
+ errors_corrected += 1
202
+ if syndrome <= 7:
203
+ r[syndrome - 1] ^= 1 # flip the error bit
204
+
205
+ # Extract data bits
206
+ decoded.extend([r[2], r[4], r[5], r[6]]) # d0, d1, d2, d3
207
+
208
+ return decoded, errors_corrected
209
+
210
+ def fec_encode(bits: List[int], scheme: FEC) -> List[int]:
211
+ if scheme == FEC.NONE:
212
+ return list(bits)
213
+ elif scheme == FEC.HAMMING74:
214
+ return hamming74_encode(bits)
215
+ elif scheme in (FEC.REED_SOLOMON, FEC.LDPC, FEC.TURBO):
216
+ raise NotImplementedError(f"{scheme.name} encoding not implemented")
217
+ else:
218
+ raise ValueError("Unknown FEC scheme")
219
+
220
+ def fec_decode(bits: List[int], scheme: FEC) -> Tuple[List[int], Dict[str, Any]]:
221
+ if scheme == FEC.NONE:
222
+ return list(bits), {"errors_corrected": 0}
223
+ elif scheme == FEC.HAMMING74:
224
+ decoded, errors = hamming74_decode(bits)
225
+ return decoded, {"errors_corrected": errors}
226
+ else:
227
+ raise NotImplementedError(f"{scheme.name} decoding not implemented")
228
+
229
+ # =========================================================
230
+ # Security and Framing
231
+ # =========================================================
232
+
233
+ def aes_gcm_encrypt(plaintext: bytes, password: str) -> bytes:
234
+ if not HAS_CRYPTO:
235
+ raise RuntimeError("pycryptodome required for encryption")
236
+
237
+ salt = get_random_bytes(16)
238
+ key = PBKDF2(password, salt, dkLen=32, count=200_000)
239
+ nonce = get_random_bytes(12)
240
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
241
+ ciphertext, tag = cipher.encrypt_and_digest(plaintext)
242
+
243
+ return b"AGCM" + salt + nonce + tag + ciphertext
244
+
245
+ def aes_gcm_decrypt(encrypted: bytes, password: str) -> bytes:
246
+ if not HAS_CRYPTO:
247
+ raise RuntimeError("pycryptodome required for decryption")
248
+
249
+ if not encrypted.startswith(b"AGCM"):
250
+ raise ValueError("Invalid encrypted format")
251
+
252
+ data = encrypted[4:] # skip "AGCM" header
253
+ salt = data[:16]
254
+ nonce = data[16:28]
255
+ tag = data[28:44]
256
+ ciphertext = data[44:]
257
+
258
+ key = PBKDF2(password, salt, dkLen=32, count=200_000)
259
+ cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
260
+
261
+ return cipher.decrypt_and_verify(ciphertext, tag)
262
+
263
+ def apply_hmac(data: bytes, hkey: str) -> bytes:
264
+ import hmac
265
+ key = hashlib.sha256(hkey.encode("utf-8")).digest()
266
+ mac = hmac.new(key, data, hashlib.sha256).digest()
267
+ return data + b"HMAC" + mac
268
+
269
+ def verify_hmac(data: bytes, hkey: str) -> Tuple[bytes, bool]:
270
+ if not data.endswith(b"HMAC"):
271
+ return data, False
272
+
273
+ # Find HMAC marker
274
+ hmac_pos = data.rfind(b"HMAC")
275
+ if hmac_pos == -1 or len(data) - hmac_pos != 36: # 4 + 32 bytes
276
+ return data, False
277
+
278
+ payload = data[:hmac_pos]
279
+ received_mac = data[hmac_pos + 4:]
280
+
281
+ import hmac
282
+ key = hashlib.sha256(hkey.encode("utf-8")).digest()
283
+ expected_mac = hmac.new(key, payload, hashlib.sha256).digest()
284
+
285
+ return payload, hmac.compare_digest(received_mac, expected_mac)
286
+
287
+ def add_watermark(data: bytes, wm: str) -> bytes:
288
+ return hashlib.sha256(wm.encode("utf-8")).digest()[:8] + data
289
+
290
+ def check_watermark(data: bytes, wm: str) -> Tuple[bytes, bool]:
291
+ if len(data) < 8:
292
+ return data, False
293
+
294
+ expected = hashlib.sha256(wm.encode("utf-8")).digest()[:8]
295
+ received = data[:8]
296
+ payload = data[8:]
297
+
298
+ return payload, received == expected
299
+
300
+ def frame_payload(payload: bytes, fcfg: FrameConfig) -> bytes:
301
+ header = struct.pack(">BBI", 0xA5, fcfg.version, now_ms() & 0xFFFFFFFF)
302
+ core = header + payload
303
+
304
+ tail = b""
305
+ if fcfg.use_crc32:
306
+ tail += crc32_bytes(core)
307
+ if fcfg.use_crc16:
308
+ tail += crc16_ccitt(core)
309
+
310
+ return fcfg.preamble + core + tail
311
+
312
+ def unframe_payload(framed: bytes, fcfg: FrameConfig) -> Tuple[bytes, Dict[str, Any]]:
313
+ if len(framed) < len(fcfg.preamble) + 7: # minimum frame size
314
+ return b"", {"error": "Frame too short"}
315
+
316
+ # Check preamble
317
+ if not framed.startswith(fcfg.preamble):
318
+ return b"", {"error": "Invalid preamble"}
319
+
320
+ data = framed[len(fcfg.preamble):]
321
+
322
+ # Parse header
323
+ if len(data) < 7:
324
+ return b"", {"error": "Header too short"}
325
+
326
+ sync, version, timestamp = struct.unpack(">BBI", data[:7])
327
+ if sync != 0xA5:
328
+ return b"", {"error": "Invalid sync byte"}
329
+
330
+ # Calculate payload length
331
+ tail_len = 0
332
+ if fcfg.use_crc32:
333
+ tail_len += 4
334
+ if fcfg.use_crc16:
335
+ tail_len += 2
336
+
337
+ if len(data) < 7 + tail_len:
338
+ return b"", {"error": "Frame too short for CRC"}
339
+
340
+ payload = data[7:-tail_len] if tail_len > 0 else data[7:]
341
+
342
+ # Verify CRCs
343
+ info = {"version": version, "timestamp": timestamp}
344
+
345
+ if fcfg.use_crc32:
346
+ expected_crc32 = crc32_bytes(data[:-tail_len])
347
+ received_crc32 = data[-tail_len:-tail_len+4] if fcfg.use_crc16 else data[-4:]
348
+ info["crc32_ok"] = expected_crc32 == received_crc32
349
+
350
+ if fcfg.use_crc16:
351
+ expected_crc16 = crc16_ccitt(data[:-2])
352
+ received_crc16 = data[-2:]
353
+ info["crc16_ok"] = expected_crc16 == received_crc16
354
+
355
+ return payload, info
356
+
357
+ def encode_text(text: str, fcfg: FrameConfig, sec: SecurityConfig, fec_scheme: FEC) -> List[int]:
358
+ """Complete encoding pipeline"""
359
+ data = text.encode("utf-8")
360
+
361
+ # Apply watermark
362
+ if sec.watermark:
363
+ data = add_watermark(data, sec.watermark)
364
+
365
+ # Apply encryption
366
+ if sec.password:
367
+ data = aes_gcm_encrypt(data, sec.password)
368
+
369
+ # Frame the data
370
+ framed = frame_payload(data, fcfg)
371
+
372
+ # Apply HMAC
373
+ if sec.hmac_key:
374
+ framed = apply_hmac(framed, sec.hmac_key)
375
+
376
+ # Convert to bits and apply FEC
377
+ bits = to_bits(framed)
378
+ bits = fec_encode(bits, fec_scheme)
379
+
380
+ return bits
381
+
382
+ def decode_bits(bits: List[int], fcfg: FrameConfig, sec: SecurityConfig, fec_scheme: FEC) -> Tuple[str, Dict[str, Any]]:
383
+ """Complete decoding pipeline"""
384
+ info = {}
385
+
386
+ try:
387
+ # Apply FEC decoding
388
+ decoded_bits, fec_info = fec_decode(bits, fec_scheme)
389
+ info.update(fec_info)
390
+
391
+ # Convert bits to bytes
392
+ framed = from_bits(decoded_bits)
393
+
394
+ # Verify HMAC
395
+ if sec.hmac_key:
396
+ framed, hmac_ok = verify_hmac(framed, sec.hmac_key)
397
+ info["hmac_ok"] = hmac_ok
398
+ if not hmac_ok:
399
+ return "", {**info, "error": "HMAC verification failed"}
400
+
401
+ # Unframe
402
+ data, frame_info = unframe_payload(framed, fcfg)
403
+ info.update(frame_info)
404
+
405
+ if "error" in frame_info:
406
+ return "", info
407
+
408
+ # Decrypt
409
+ if sec.password:
410
+ data = aes_gcm_decrypt(data, sec.password)
411
+ info["decrypted"] = True
412
+
413
+ # Check watermark
414
+ if sec.watermark:
415
+ data, wm_ok = check_watermark(data, sec.watermark)
416
+ info["watermark_ok"] = wm_ok
417
+ if not wm_ok:
418
+ return "", {**info, "error": "Watermark verification failed"}
419
+
420
+ # Decode text
421
+ text = data.decode("utf-8", errors="replace")
422
+ return text, info
423
+
424
+ except Exception as e:
425
+ return "", {**info, "error": str(e)}
426
+
427
+ # =========================================================
428
+ # Modulation Schemes
429
+ # =========================================================
430
+
431
+ class Modulators:
432
+ @staticmethod
433
+ def bfsk(bits: Sequence[int], cfg: ModConfig) -> np.ndarray:
434
+ """Binary Frequency Shift Keying"""
435
+ sr, rb = cfg.sample_rate, cfg.symbol_rate
436
+ spb = int(sr / rb) # samples per bit
437
+ t = np.arange(spb) / sr
438
+
439
+ signal_blocks = []
440
+ for bit in bits:
441
+ freq = cfg.f1 if bit else cfg.f0
442
+ signal_blocks.append(cfg.amplitude * np.sin(2 * np.pi * freq * t))
443
+
444
+ if not signal_blocks:
445
+ return np.zeros(0, dtype=np.float32)
446
+
447
+ signal = np.concatenate(signal_blocks)
448
+
449
+ if cfg.clip:
450
+ signal = np.clip(signal, -1, 1)
451
+
452
+ return signal.astype(np.float32)
453
+
454
+ @staticmethod
455
+ def bpsk(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
456
+ """Binary Phase Shift Keying"""
457
+ sr, rb, fc = cfg.sample_rate, cfg.symbol_rate, cfg.fc
458
+ spb = int(sr / rb)
459
+ t = np.arange(spb) / sr
460
+
461
+ audio_blocks = []
462
+ iq_blocks = []
463
+
464
+ for bit in bits:
465
+ phase = 0.0 if bit else np.pi
466
+
467
+ # Audio signal (upconverted)
468
+ audio_blocks.append(cfg.amplitude * np.sin(2 * np.pi * fc * t + phase))
469
+
470
+ # IQ signal (baseband)
471
+ iq_symbol = cfg.amplitude * (np.cos(phase) + 1j * np.sin(phase))
472
+ iq_blocks.append(iq_symbol * np.ones(spb, dtype=np.complex64))
473
+
474
+ audio = np.concatenate(audio_blocks) if audio_blocks else np.zeros(0, dtype=np.float32)
475
+ iq = np.concatenate(iq_blocks) if iq_blocks else np.zeros(0, dtype=np.complex64)
476
+
477
+ if cfg.clip:
478
+ audio = np.clip(audio, -1, 1)
479
+
480
+ return audio.astype(np.float32), iq
481
+
482
+ @staticmethod
483
+ def qpsk(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
484
+ """Quadrature Phase Shift Keying"""
485
+ pairs = chunk_bits(bits, 2)
486
+ symbols = []
487
+
488
+ # Gray mapping: 00→(1+1j), 01→(-1+1j), 11→(-1-1j), 10→(1-1j)
489
+ for pair in pairs:
490
+ b0, b1 = (pair + [0, 0])[:2]
491
+ if (b0, b1) == (0, 0):
492
+ symbol = 1 + 1j
493
+ elif (b0, b1) == (0, 1):
494
+ symbol = -1 + 1j
495
+ elif (b0, b1) == (1, 1):
496
+ symbol = -1 - 1j
497
+ else: # (1, 0)
498
+ symbol = 1 - 1j
499
+
500
+ symbols.append(symbol / math.sqrt(2)) # normalize for unit energy
501
+
502
+ return Modulators._psk_qam_to_audio_iq(np.array(symbols, dtype=np.complex64), cfg)
503
+
504
+ @staticmethod
505
+ def qam16(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
506
+ """16-QAM modulation"""
507
+ quads = chunk_bits(bits, 4)
508
+
509
+ def gray_map_2bit(b0, b1):
510
+ # Gray mapping for 2 bits to {-3, -1, 1, 3}
511
+ val = (b0 << 1) | b1
512
+ return [-3, -1, 1, 3][val]
513
+
514
+ symbols = []
515
+ for quad in quads:
516
+ b0, b1, b2, b3 = (quad + [0, 0, 0, 0])[:4]
517
+ I = gray_map_2bit(b0, b1)
518
+ Q = gray_map_2bit(b2, b3)
519
+ symbol = (I + 1j * Q) / math.sqrt(10) # normalize for unit average power
520
+ symbols.append(symbol)
521
+
522
+ return Modulators._psk_qam_to_audio_iq(np.array(symbols, dtype=np.complex64), cfg)
523
+
524
+ @staticmethod
525
+ def _psk_qam_to_audio_iq(symbols: np.ndarray, cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
526
+ """Convert PSK/QAM symbols to audio and IQ signals"""
527
+ sr, rb, fc = cfg.sample_rate, cfg.symbol_rate, cfg.fc
528
+ spb = int(sr / rb)
529
+
530
+ # Upsample symbols (rectangular pulse shaping)
531
+ i_data = np.repeat(symbols.real.astype(np.float32), spb)
532
+ q_data = np.repeat(symbols.imag.astype(np.float32), spb)
533
+
534
+ # Generate time vector
535
+ t = np.arange(len(i_data)) / sr
536
+
537
+ # Generate audio signal (upconverted)
538
+ audio = cfg.amplitude * (i_data * np.cos(2 * np.pi * fc * t) -
539
+ q_data * np.sin(2 * np.pi * fc * t))
540
+
541
+ # Generate IQ signal (baseband)
542
+ iq = (cfg.amplitude * i_data) + 1j * (cfg.amplitude * q_data)
543
+
544
+ if cfg.clip:
545
+ audio = np.clip(audio, -1, 1)
546
+
547
+ return audio.astype(np.float32), iq.astype(np.complex64)
548
+
549
+ @staticmethod
550
+ def afsk(bits: Sequence[int], cfg: ModConfig) -> np.ndarray:
551
+ """Audio Frequency Shift Keying (same as BFSK)"""
552
+ return Modulators.bfsk(bits, cfg)
553
+
554
+ @staticmethod
555
+ def dsss_bpsk(bits: Sequence[int], cfg: ModConfig) -> np.ndarray:
556
+ """Direct Sequence Spread Spectrum BPSK"""
557
+ # Simple PN sequence for spreading
558
+ pn_sequence = np.array([1, -1, 1, 1, -1, 1, -1, -1], dtype=np.float32)
559
+
560
+ sr = cfg.sample_rate
561
+ chip_rate = cfg.dsss_chip_rate
562
+ samples_per_chip = int(sr / chip_rate)
563
+
564
+ baseband_signal = []
565
+
566
+ for bit in bits:
567
+ bit_value = 1.0 if bit else -1.0
568
+
569
+ # Spread with PN sequence
570
+ spread_chips = bit_value * pn_sequence
571
+
572
+ # Upsample chips
573
+ for chip in spread_chips:
574
+ baseband_signal.extend([chip] * samples_per_chip)
575
+
576
+ baseband = np.array(baseband_signal, dtype=np.float32)
577
+
578
+ # Upconvert to carrier frequency
579
+ t = np.arange(len(baseband)) / sr
580
+ audio = cfg.amplitude * baseband * np.sin(2 * np.pi * cfg.fc * t)
581
+
582
+ if cfg.clip:
583
+ audio = np.clip(audio, -1, 1)
584
+
585
+ return audio.astype(np.float32)
586
+
587
+ @staticmethod
588
+ def ofdm(bits: Sequence[int], cfg: ModConfig) -> Tuple[np.ndarray, np.ndarray]:
589
+ """Orthogonal Frequency Division Multiplexing"""
590
+ N = cfg.ofdm_subc
591
+ cp_len = cfg.cp_len
592
+
593
+ # Group bits for QPSK mapping on each subcarrier
594
+ symbol_chunks = chunk_bits(bits, 2 * N)
595
+
596
+ audio_blocks = []
597
+ iq_blocks = []
598
+
599
+ for chunk in symbol_chunks:
600
+ # Map bits to QPSK symbols
601
+ qpsk_symbols = []
602
+ bit_pairs = chunk_bits(chunk, 2)
603
+
604
+ for pair in bit_pairs:
605
+ b0, b1 = (pair + [0, 0])[:2]
606
+ if (b0, b1) == (0, 0):
607
+ symbol = 1 + 1j
608
+ elif (b0, b1) == (0, 1):
609
+ symbol = -1 + 1j
610
+ elif (b0, b1) == (1, 1):
611
+ symbol = -1 - 1j
612
+ else:
613
+ symbol = 1 - 1j
614
+ qpsk_symbols.append(symbol / math.sqrt(2))
615
+
616
+ # Pad to N subcarriers
617
+ while len(qpsk_symbols) < N:
618
+ qpsk_symbols.append(0j)
619
+
620
+ # IFFT to get time domain signal
621
+ freq_domain = np.array(qpsk_symbols[:N], dtype=np.complex64)
622
+ time_domain = np.fft.ifft(freq_domain)
623
+
624
+ # Add cyclic prefix
625
+ cyclic_prefix = time_domain[-cp_len:]
626
+ ofdm_symbol = np.concatenate([cyclic_prefix, time_domain])
627
+
628
+ # Scale to fit symbol rate timing
629
+ symbol_duration = int(cfg.sample_rate / cfg.symbol_rate)
630
+ repeat_factor = max(1, symbol_duration // len(ofdm_symbol))
631
+ upsampled = np.repeat(ofdm_symbol, repeat_factor)
632
+
633
+ # Generate audio (upconverted)
634
+ t = np.arange(len(upsampled)) / cfg.sample_rate
635
+ audio = cfg.amplitude * (upsampled.real * np.cos(2 * np.pi * cfg.fc * t) -
636
+ upsampled.imag * np.sin(2 * np.pi * cfg.fc * t))
637
+
638
+ audio_blocks.append(audio.astype(np.float32))
639
+ iq_blocks.append((cfg.amplitude * upsampled).astype(np.complex64))
640
+
641
+ audio = np.concatenate(audio_blocks) if audio_blocks else np.zeros(0, dtype=np.float32)
642
+ iq = np.concatenate(iq_blocks) if iq_blocks else np.zeros(0, dtype=np.complex64)
643
+
644
+ if cfg.clip:
645
+ audio = np.clip(audio, -1, 1)
646
+
647
+ return audio, iq
648
+
649
+ def bits_to_signals(bits: List[int], scheme: ModulationScheme, cfg: ModConfig) -> Tuple[Optional[np.ndarray], Optional[np.ndarray]]:
650
+ """Convert bits to modulated signals"""
651
+ if scheme == ModulationScheme.BFSK:
652
+ return Modulators.bfsk(bits, cfg), None
653
+ elif scheme == ModulationScheme.AFSK:
654
+ return Modulators.afsk(bits, cfg), None
655
+ elif scheme == ModulationScheme.BPSK:
656
+ return Modulators.bpsk(bits, cfg)
657
+ elif scheme == ModulationScheme.QPSK:
658
+ return Modulators.qpsk(bits, cfg)
659
+ elif scheme == ModulationScheme.QAM16:
660
+ return Modulators.qam16(bits, cfg)
661
+ elif scheme == ModulationScheme.OFDM:
662
+ return Modulators.ofdm(bits, cfg)
663
+ elif scheme == ModulationScheme.DSSS_BPSK:
664
+ return Modulators.dsss_bpsk(bits, cfg), None
665
+ else:
666
+ raise ValueError(f"Unknown modulation scheme: {scheme}")
667
+
668
+ # =========================================================
669
+ # File I/O and Visualization
670
+ # =========================================================
671
+
672
+ def write_wav_mono(path: Path, signal: np.ndarray, sample_rate: int):
673
+ """Write mono WAV file"""
674
+ sig = np.clip(signal, -1.0, 1.0)
675
+ pcm = (sig * 32767.0).astype(np.int16)
676
+
677
+ with wave.open(str(path), "wb") as w:
678
+ w.setnchannels(1)
679
+ w.setsampwidth(2)
680
+ w.setframerate(sample_rate)
681
+ w.writeframes(pcm.tobytes())
682
+
683
+ def write_iq_f32(path: Path, iq: np.ndarray):
684
+ """Write IQ data as interleaved float32"""
685
+ if iq.ndim != 1 or not np.iscomplexobj(iq):
686
+ raise ValueError("iq must be 1-D complex array")
687
+
688
+ interleaved = np.empty(iq.size * 2, dtype=np.float32)
689
+ interleaved[0::2] = iq.real.astype(np.float32)
690
+ interleaved[1::2] = iq.imag.astype(np.float32)
691
+
692
+ path.write_bytes(interleaved.tobytes())
693
+
694
+ def plot_wave_and_spectrum(path_png: Path, x: np.ndarray, sr: int, title: str):
695
+ """Plot waveform and spectrum"""
696
+ if not HAS_MATPLOTLIB:
697
+ logger.warning("Matplotlib not available, skipping plot")
698
+ return
699
+
700
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
701
+
702
+ # Time domain plot (first 50ms)
703
+ samples_to_plot = min(len(x), int(0.05 * sr))
704
+ t = np.arange(samples_to_plot) / sr
705
+ ax1.plot(t, x[:samples_to_plot])
706
+ ax1.set_title(f"{title} - Time Domain (first 50ms)")
707
+ ax1.set_xlabel("Time (s)")
708
+ ax1.set_ylabel("Amplitude")
709
+ ax1.grid(True, alpha=0.3)
710
+
711
+ # Frequency domain plot
712
+ spectrum = np.abs(rfft(x)) + 1e-12
713
+ freqs = rfftfreq(len(x), 1.0 / sr)
714
+ ax2.semilogy(freqs, spectrum / spectrum.max())
715
+ ax2.set_xlim(0, min(8000, sr // 2))
716
+ ax2.set_title(f"{title} - Frequency Domain")
717
+ ax2.set_xlabel("Frequency (Hz)")
718
+ ax2.set_ylabel("Normalized |X(f)|")
719
+ ax2.grid(True, alpha=0.3)
720
+
721
+ plt.tight_layout()
722
+ fig.savefig(path_png, dpi=300, bbox_inches='tight')
723
+ plt.close(fig)
724
+
725
+ def plot_constellation(symbols: np.ndarray, title: str = "Constellation", save_path: Optional[str] = None):
726
+ """Plot constellation diagram"""
727
+ if not HAS_MATPLOTLIB:
728
+ logger.warning("Matplotlib not available, skipping constellation plot")
729
+ return
730
+
731
+ plt.figure(figsize=(8, 8))
732
+ plt.scatter(np.real(symbols), np.imag(symbols), alpha=0.7, s=20)
733
+ plt.title(title)
734
+ plt.xlabel("In-phase (I)")
735
+ plt.ylabel("Quadrature (Q)")
736
+ plt.grid(True, alpha=0.3)
737
+ plt.axis('equal')
738
+
739
+ if save_path:
740
+ plt.savefig(save_path, dpi=300, bbox_inches='tight')
741
+ plt.close()
742
+ else:
743
+ plt.show()
744
+
745
+ def play_audio(x: np.ndarray, sr: int):
746
+ """Play audio through soundcard"""
747
+ if not HAS_AUDIO:
748
+ logger.warning("sounddevice not installed; cannot play audio")
749
+ return
750
+
751
+ try:
752
+ sd.play(x, sr)
753
+ sd.wait()
754
+ except Exception as e:
755
+ logger.error(f"Audio playback failed: {e}")
756
+
757
+ # =========================================================
758
+ # Complete Processing Pipeline
759
+ # =========================================================
760
+
761
+ def full_process_and_save(
762
+ text: str,
763
+ outdir: Path,
764
+ scheme: ModulationScheme,
765
+ mcfg: ModConfig,
766
+ fcfg: FrameConfig,
767
+ sec: SecurityConfig,
768
+ fec_scheme: FEC,
769
+ want_wav: bool,
770
+ want_iq: bool,
771
+ title: str = "SignalProcessor"
772
+ ) -> OutputPaths:
773
+ """Complete processing pipeline from text to files"""
774
+
775
+ outdir.mkdir(parents=True, exist_ok=True)
776
+ timestamp = int(time.time())
777
+ base_name = f"signal_{scheme.name.lower()}_{timestamp}"
778
+ base_path = outdir / base_name
779
+
780
+ # Encode text to bits
781
+ bits = encode_text(text, fcfg, sec, fec_scheme)
782
+ logger.info(f"Encoded {len(text)} characters to {len(bits)} bits")
783
+
784
+ # Modulate bits to signals
785
+ audio, iq = bits_to_signals(bits, scheme, mcfg)
786
+
787
+ paths = OutputPaths()
788
+
789
+ # Save WAV file
790
+ if want_wav and audio is not None and len(audio) > 0:
791
+ paths.wav = base_path.with_suffix(".wav")
792
+ write_wav_mono(paths.wav, audio, mcfg.sample_rate)
793
+ logger.info(f"Saved WAV: {paths.wav}")
794
+
795
+ # Save IQ file
796
+ if want_iq:
797
+ if iq is None and audio is not None:
798
+ # Generate IQ from audio using Hilbert transform
799
+ try:
800
+ analytic = sp_signal.hilbert(audio)
801
+ iq = analytic.astype(np.complex64)
802
+ except Exception as e:
803
+ logger.warning(f"Failed to generate IQ from audio: {e}")
804
+ iq = audio.astype(np.float32) + 1j * np.zeros_like(audio, dtype=np.float32)
805
+
806
+ if iq is not None:
807
+ paths.iq = base_path.with_suffix(".iqf32")
808
+ write_iq_f32(paths.iq, iq)
809
+ logger.info(f"Saved IQ: {paths.iq}")
810
+
811
+ # Generate visualization
812
+ if audio is not None and len(audio) > 0:
813
+ paths.png = base_path.with_suffix(".png")
814
+ plot_wave_and_spectrum(paths.png, audio, mcfg.sample_rate, title)
815
+ logger.info(f"Saved plot: {paths.png}")
816
+
817
+ # Save metadata
818
+ metadata = {
819
+ "timestamp": timestamp,
820
+ "scheme": scheme.name,
821
+ "sample_rate": mcfg.sample_rate,
822
+ "symbol_rate": mcfg.symbol_rate,
823
+ "duration_sec": len(audio) / mcfg.sample_rate if audio is not None else 0,
824
+ "fec": fec_scheme.name,
825
+ "encrypted": bool(sec.password),
826
+ "watermark": bool(sec.watermark),
827
+ "hmac": bool(sec.hmac_key),
828
+ "text_length": len(text),
829
+ "bits_length": len(bits)
830
+ }
831
+
832
+ paths.meta = base_path.with_suffix(".json")
833
+ paths.meta.write_text(safe_json(metadata), encoding="utf-8")
834
+ logger.info(f"Saved metadata: {paths.meta}")
835
+
836
+ return paths
837
+
838
+ def demo_signal_processing():
839
+ """Demonstration of signal processing capabilities"""
840
+
841
+ # Test configuration
842
+ text = "Hello, World! This is a test of the signal processing system. 🚀"
843
+
844
+ schemes_to_test = [
845
+ ModulationScheme.BFSK,
846
+ ModulationScheme.QPSK,
847
+ ModulationScheme.QAM16,
848
+ ModulationScheme.OFDM
849
+ ]
850
+
851
+ mcfg = ModConfig(sample_rate=48000, symbol_rate=1200)
852
+ fcfg = FrameConfig()
853
+ sec = SecurityConfig(watermark="test_watermark")
854
+ fec_scheme = FEC.HAMMING74
855
+
856
+ results = []
857
+
858
+ for scheme in schemes_to_test:
859
+ logger.info(f"Testing {scheme.name}...")
860
+
861
+ try:
862
+ paths = full_process_and_save(
863
+ text=text,
864
+ outdir=Path("demo_output"),
865
+ scheme=scheme,
866
+ mcfg=mcfg,
867
+ fcfg=fcfg,
868
+ sec=sec,
869
+ fec_scheme=fec_scheme,
870
+ want_wav=True,
871
+ want_iq=True,
872
+ title=f"{scheme.name} Demo"
873
+ )
874
+
875
+ results.append({
876
+ "scheme": scheme.name,
877
+ "success": True,
878
+ "paths": paths
879
+ })
880
+
881
+ except Exception as e:
882
+ logger.error(f"Failed to process {scheme.name}: {e}")
883
+ results.append({
884
+ "scheme": scheme.name,
885
+ "success": False,
886
+ "error": str(e)
887
+ })
888
+
889
+ # Print summary
890
+ logger.info("=== Signal Processing Demo Complete ===")
891
+ for result in results:
892
+ status = "✓" if result["success"] else "✗"
893
+ logger.info(f"{status} {result['scheme']}")
894
+
895
+ return results
896
+
897
+ if __name__ == "__main__":
898
+ demo_signal_processing()