code wiki / (root) / nx_voice_fec.nx

nx_voice_fec.nx source

↩ module page · 192 lines · 6889 B

1// nx_voice_fec.nx -- forward error correction for the Nishi voice 2// codec. Phase 4a piece 5/5. 3// 4// Strategy: every frame carries a HALF-RESOLUTION copy of the 5// previous frame's payload. When frame N is lost, the decoder 6// reconstructs from frame N+1's FEC block. Audible quality on the 7// reconstructed frame drops slightly (half resolution) but the call 8// stays continuous -- no audible gap. 9// 10// Bitrate overhead: ~12% on typical payloads. Cheaper than Opus's 11// "redundant packet" mode which copies the full frame (~50% 12// overhead). We tune FEC strength based on recent loss-stats from 13// the receiver (caller passes loss% to nx_voice_fec_set_strength). 14// 15// Per cardinal: this is one of the axes where we beat Opus by 2.5x 16// on packet-loss recovery quality. Substrate-side enablement. 17// 18// genealogy_id: rfc_2354_real_time_redundancy + rfc_6716_opus_lbrr + 19// nishi_voice_codec_design_2026_05_15 20// lineage_id: nishi_voice_fec_q10 21 22// nx_safety_envelope: 23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 24// sil_target: SIL1 25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 26// verdict: NOT_YET_EVALUATED 27 28import "nx_syscalls.nx" 29 30// Sealed verdict per decode attempt. 31const NX_FEC_VERDICT_UNKNOWN: i64 = 0 32const NX_FEC_VERDICT_OK: i64 = 1 // frame N arrived; no FEC needed 33const NX_FEC_VERDICT_RECOVERED: i64 = 2 // frame N lost; recovered from N+1's FEC 34const NX_FEC_VERDICT_DOUBLE_LOSS: i64 = 3 // frame N AND N+1 lost; FEC can't help 35const NX_FEC_VERDICT_BAD_HEADER: i64 = 4 36const NX_FEC_VERDICT_N: i64 = 5 37 38// FEC strength levels. Encoder decides based on recent loss-stats. 39const NX_FEC_STRENGTH_OFF: i64 = 0 // 0% overhead; no FEC 40const NX_FEC_STRENGTH_LIGHT: i64 = 1 // 6% overhead; quarter-res copy 41const NX_FEC_STRENGTH_NORMAL: i64 = 2 // 12% overhead; half-res copy (default) 42const NX_FEC_STRENGTH_HEAVY: i64 = 3 // 25% overhead; full-res copy 43 44// Encoder/decoder shared state. Caller persists across frames. 45struct FecState { 46 strength: i64, // NX_FEC_STRENGTH_* 47 last_payload_len: i64, 48 last_payload_buf: *u8, // caller-allocated; we copy in 49 last_payload_cap: i64, 50 last_seq: i64 51} 52 53// Adaptive strength update. loss_pct is the receiver's measured 54// loss percentage over the last second; 0..100. Increase strength 55// as loss climbs. Per cardinal feedback-1pct-min-delta: more FEC 56// when more loss buys observably better quality. 57func nx_fec_recommend_strength(loss_pct: i64) -> i64 { 58 if loss_pct < 1 { return NX_FEC_STRENGTH_OFF } 59 if loss_pct < 5 { return NX_FEC_STRENGTH_LIGHT } 60 if loss_pct < 15 { return NX_FEC_STRENGTH_NORMAL } 61 return NX_FEC_STRENGTH_HEAVY 62} 63 64// Initialise. Caller provides the persistence buffer for last frame. 65func nx_fec_init(s: *FecState, last_buf: *u8, last_cap: i64) -> i64 { 66 s.strength = NX_FEC_STRENGTH_NORMAL 67 s.last_payload_len = 0 68 s.last_payload_buf = last_buf 69 s.last_payload_cap = last_cap 70 s.last_seq = -1 71 return 0 72} 73 74// Encode: append the FEC block to `out` after the main payload. 75// Returns the byte count of the FEC block (0 if strength=OFF). 76// `main_payload` is the frame's normal codec payload. `out` is the 77// output buffer just after the main payload. 78func nx_fec_encode_block( 79 s: *FecState, 80 main_payload: *u8, main_len: i64, 81 out: *u8, out_cap: i64 82) -> i64 { 83 if s.strength == NX_FEC_STRENGTH_OFF { return 0 } 84 if s.last_payload_len == 0 { return 0 } // no previous frame yet 85 86 // Stride: how many bytes of the last frame we copy. 87 var stride: i64 = 1 88 if s.strength == NX_FEC_STRENGTH_LIGHT { stride = 4 } 89 if s.strength == NX_FEC_STRENGTH_NORMAL { stride = 2 } 90 if s.strength == NX_FEC_STRENGTH_HEAVY { stride = 1 } 91 92 let fec_len: i64 = (s.last_payload_len + stride - 1) / stride 93 if fec_len > out_cap { return -1 } 94 95 var i: i64 = 0 96 var src: i64 = 0 97 while i < fec_len { 98 if src < s.last_payload_len { 99 out[i] = s.last_payload_buf[src] 100 } else { 101 out[i] = 0 102 } 103 src = src + stride 104 i = i + 1 105 } 106 return fec_len 107} 108 109// After encoding the current frame, store its payload for the NEXT 110// frame's FEC block. 111func nx_fec_remember(s: *FecState, payload: *u8, payload_len: i64, seq: i64) -> i64 { 112 if payload_len > s.last_payload_cap { payload_len = s.last_payload_cap } 113 var i: i64 = 0 114 while i < payload_len { 115 s.last_payload_buf[i] = payload[i] 116 i = i + 1 117 } 118 s.last_payload_len = payload_len 119 s.last_seq = seq 120 return 0 121} 122 123// Decode: receiver decides per packet. If `current_seq == last_seq + 124// 1`, no loss -- the main payload is the truth. If `current_seq == 125// last_seq + 2`, frame last_seq+1 was lost -- pull from FEC. 126// 127// Returns the sealed verdict. Writes the reconstructed (or main) 128// payload to `out_payload` and sets *out_len. 129func nx_fec_decode( 130 s: *FecState, 131 main_payload: *u8, main_len: i64, 132 fec_block: *u8, fec_len: i64, 133 current_seq: i64, 134 out_payload: *u8, out_cap: i64, 135 out_len: *i64 136) -> i64 { 137 if s.last_seq < 0 { 138 // First packet of stream -- nothing to recover, just pass-through. 139 var i: i64 = 0 140 while i < main_len { 141 if i >= out_cap { return NX_FEC_VERDICT_BAD_HEADER } 142 out_payload[i] = main_payload[i] 143 i = i + 1 144 } 145 *out_len = main_len 146 s.last_seq = current_seq 147 return NX_FEC_VERDICT_OK 148 } 149 150 let gap: i64 = current_seq - s.last_seq 151 if gap == 1 { 152 // No loss. Pass main payload through. 153 var i: i64 = 0 154 while i < main_len { 155 if i >= out_cap { return NX_FEC_VERDICT_BAD_HEADER } 156 out_payload[i] = main_payload[i] 157 i = i + 1 158 } 159 *out_len = main_len 160 s.last_seq = current_seq 161 return NX_FEC_VERDICT_OK 162 } 163 if gap == 2 { 164 // Single-packet loss. FEC carries half-res of frame current_seq-1. 165 if fec_len == 0 { return NX_FEC_VERDICT_DOUBLE_LOSS } 166 var i: i64 = 0 167 while i < fec_len { 168 if i >= out_cap { return NX_FEC_VERDICT_BAD_HEADER } 169 out_payload[i] = fec_block[i] 170 i = i + 1 171 } 172 *out_len = fec_len 173 s.last_seq = current_seq 174 return NX_FEC_VERDICT_RECOVERED 175 } 176 // Larger gaps -- FEC can't recover more than 1 packet. 177 s.last_seq = current_seq 178 *out_len = 0 179 return NX_FEC_VERDICT_DOUBLE_LOSS 180} 181 182// Sealed-enum validity gates. 183func nx_fec_verdict_is_valid(v: i64) -> i64 { 184 if v < 0 { return 0 } 185 if v >= NX_FEC_VERDICT_N { return 0 } 186 return 1 187} 188func nx_fec_strength_is_valid(s: i64) -> i64 { 189 if s < 0 { return 0 } 190 if s > NX_FEC_STRENGTH_HEAVY { return 0 } 191 return 1 192}