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}