code wiki / (root) / nx_bfsk.nx

nx_bfsk.nx source

↩ module page · 162 lines · 6186 B

1// nx_bfsk.nx -- Binary Frequency Shift Keying modem. Apparatus 2// substrate piece between the codec layer (bytes) and the radio layer 3// (samples). Integer-only, Q10 phase, square-wave reference signals 4// for fully-bits-up correlator demodulation. 5// 6// Doctrinal stance: this is the primitive that lets NishiVoice frames 7// modulate onto an HF carrier driving a ground dipole, or onto a UHF 8// carrier driving a chirp-spread-spectrum mesh. No external DSP 9// library; the modulator IS the math; the demod IS the math. 10// 11// Math (public domain since Hartley 1928): 12// bit 0 -> N samples of a square wave at frequency f0 13// bit 1 -> N samples of a square wave at frequency f1 14// demod -> correlate received samples against reference square wave 15// at f0 and f1; pick whichever correlation is larger. 16// 17// Phase accumulator is Q10 (1024 units per period), so phase_inc_q10 18// for frequency f at sample rate fs = round(f * 1024 / fs). At fs= 19// 8000 and f0=1200, phase_inc_0 = 154; at f1=2200, phase_inc_1=282. 20// 21// genealogy_id: hartley_1928_modulation + bell_202_1962_fsk_audio + 22// nx_voice_codec_v1_q10 23// lineage_id: nishi_bfsk_q10 24 25// nx_safety_envelope: 26// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 27// sil_target: SIL1 28// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 29// verdict: NOT_YET_EVALUATED 30 31import "nx_syscalls_x86_64.nx" 32 33// Sealed verdict per modulate/demodulate call. 34const NX_BFSK_VERDICT_UNKNOWN: i64 = 0 35const NX_BFSK_VERDICT_OK: i64 = 1 36const NX_BFSK_VERDICT_BUF_TOO_SMALL: i64 = 2 37const NX_BFSK_VERDICT_BAD_PARAMS: i64 = 3 38const NX_BFSK_VERDICT_SNR_LOW: i64 = 4 // demod ran but correlation margin too small 39const NX_BFSK_VERDICT_N: i64 = 5 40 41const NX_BFSK_PHASE_PERIOD: i64 = 1024 42const NX_BFSK_PHASE_HALF: i64 = 512 43const NX_BFSK_AMPLITUDE: i64 = 32767 // int16 peak 44 45// Compute phase_inc_q10 given carrier f (Hz) and sample rate fs (Hz). 46// phase_inc = round(f * 1024 / fs). At least 1 to avoid DC. 47func nx_bfsk_phase_inc(f_hz: i64, fs_hz: i64) -> i64 { 48 if fs_hz <= 0 { return 0 } 49 var p: i64 = (f_hz * NX_BFSK_PHASE_PERIOD) / fs_hz 50 if p < 1 { p = 1 } 51 if p >= NX_BFSK_PHASE_HALF { p = NX_BFSK_PHASE_HALF - 1 } 52 return p 53} 54 55// Square-wave reference sample for sample index `i` given phase 56// increment. Returns +1 or -1 (caller multiplies by amplitude). 57func _ref_sign(i: i64, phase_inc: i64) -> i64 { 58 let phase: i64 = (i * phase_inc) & (NX_BFSK_PHASE_PERIOD - 1) 59 if phase < NX_BFSK_PHASE_HALF { return 1 } 60 return -1 61} 62 63// Modulate `n_bits` bits (packed as bytes in `bits`, bit 0 of each 64// byte = the actual bit) into `n_bits * samples_per_bit` int16-range 65// samples written to `out`. Returns sealed verdict; writes total 66// samples produced to *out_n. 67func nx_bfsk_modulate( 68 bits: *u8, n_bits: i64, samples_per_bit: i64, 69 phase_inc_0: i64, phase_inc_1: i64, 70 out: *i64, out_cap: i64, out_n: *i64 71) -> i64 { 72 if n_bits < 0 { return NX_BFSK_VERDICT_BAD_PARAMS } 73 if samples_per_bit < 4 { return NX_BFSK_VERDICT_BAD_PARAMS } 74 if phase_inc_0 < 1 { return NX_BFSK_VERDICT_BAD_PARAMS } 75 if phase_inc_1 < 1 { return NX_BFSK_VERDICT_BAD_PARAMS } 76 let total: i64 = n_bits * samples_per_bit 77 if total > out_cap { return NX_BFSK_VERDICT_BUF_TOO_SMALL } 78 79 var b: i64 = 0 80 while b < n_bits { 81 let bit: i64 = bits[b] & 1 82 var p_inc: i64 = phase_inc_0 83 if bit == 1 { p_inc = phase_inc_1 } 84 var i: i64 = 0 85 while i < samples_per_bit { 86 let s: i64 = _ref_sign(i, p_inc) * NX_BFSK_AMPLITUDE 87 out[b * samples_per_bit + i] = s 88 i = i + 1 89 } 90 b = b + 1 91 } 92 *out_n = total 93 return NX_BFSK_VERDICT_OK 94} 95 96// Non-coherent correlator demodulation. For each bit window, compute 97// the absolute correlation of received samples vs reference square 98// wave at f0 and f1; output bit = 1 iff |corr_1| > |corr_0|. 99// 100// We also report the smallest margin observed across the message in 101// *out_min_margin -- the operator can use that to decide if the link 102// is healthy or if it should fall back to a slower modulation. 103// 104// Returns sealed verdict. Writes decoded bits to `out_bits` (1 byte 105// per bit) and count to *out_n. 106func nx_bfsk_demodulate( 107 samples: *i64, n_samples: i64, samples_per_bit: i64, 108 phase_inc_0: i64, phase_inc_1: i64, 109 out_bits: *u8, out_cap: i64, out_n: *i64, 110 out_min_margin: *i64 111) -> i64 { 112 if samples_per_bit < 4 { return NX_BFSK_VERDICT_BAD_PARAMS } 113 if phase_inc_0 < 1 { return NX_BFSK_VERDICT_BAD_PARAMS } 114 if phase_inc_1 < 1 { return NX_BFSK_VERDICT_BAD_PARAMS } 115 let n_bits: i64 = n_samples / samples_per_bit 116 if n_bits > out_cap { return NX_BFSK_VERDICT_BUF_TOO_SMALL } 117 118 var min_margin: i64 = 0x7fffffffffffffff 119 var b: i64 = 0 120 while b < n_bits { 121 var corr_0: i64 = 0 122 var corr_1: i64 = 0 123 var i: i64 = 0 124 while i < samples_per_bit { 125 let s: i64 = samples[b * samples_per_bit + i] 126 corr_0 = corr_0 + s * _ref_sign(i, phase_inc_0) 127 corr_1 = corr_1 + s * _ref_sign(i, phase_inc_1) 128 i = i + 1 129 } 130 var a0: i64 = corr_0 131 if a0 < 0 { a0 = -a0 } 132 var a1: i64 = corr_1 133 if a1 < 0 { a1 = -a1 } 134 var bit: i64 = 0 135 if a1 > a0 { bit = 1 } 136 out_bits[b] = bit & 0xff 137 138 var margin: i64 = a1 - a0 139 if margin < 0 { margin = -margin } 140 if margin < min_margin { min_margin = margin } 141 142 b = b + 1 143 } 144 *out_n = n_bits 145 *out_min_margin = min_margin 146 147 // SNR floor: minimum margin must be at least ~1/16 of the 148 // peak-correlation magnitude. Peak correlation magnitude in the 149 // clean case is samples_per_bit * AMPLITUDE. Heuristic only. 150 let peak: i64 = samples_per_bit * NX_BFSK_AMPLITUDE 151 if min_margin * 16 < peak { 152 return NX_BFSK_VERDICT_SNR_LOW 153 } 154 return NX_BFSK_VERDICT_OK 155} 156 157// Sealed-enum validity gate. 158func nx_bfsk_verdict_is_valid(v: i64) -> i64 { 159 if v < 0 { return 0 } 160 if v >= NX_BFSK_VERDICT_N { return 0 } 161 return 1 162}