code wiki / (root) / nx_voice_codec.nx

nx_voice_codec.nx source

↩ module page · 353 lines · 13110 B

1// nx_voice_codec.nx -- NishiVoice v1: the composition primitive that 2// orchestrates VAD + LPC + residual quantisation + FEC + frame into 3// a single named sovereign voice codec. No Opus dependency. No 4// external code. Pure composition of nxc2 substrate primitives. 5// 6// genealogy_id: nx_voice_frame_q10 + nx_lpc_q15 + nx_vad_q10 + 7// nx_voice_fec_q10 + nx_voice_codec_tier_dispatch_q10 8// lineage_id: nishi_voice_codec_v1_q10 9// 10// Composition pipeline (encode): 11// PCM samples (Q-scale i64, frame-sized) 12// -> nx_vad_classify -> verdict (SILENCE / VOICE / NOISE / ...) 13// -> if SILENCE: payload = energy_byte (1 byte) + cn_seed (1) 14// -> else: nx_lpc_analyze -> order-N coefficients in Q15 15// residual = 8 evenly-spaced subsamples (16-bit each) 16// payload = [vad][order][coeffs * 2N][n_sub][sub * 2*n_sub] 17// -> nx_voice_frame_build -> wire frame with hash-chain header 18// -> nx_fec_encode_block appends FEC tail (half-res of previous frame) 19// 20// Decode reverses: 21// frame bytes -> nx_voice_frame_parse -> payload bytes 22// -> if vad=SILENCE: synthesise comfort noise samples 23// -> else: read coeffs + subsamples; expand via 24// piecewise linear interpolation 25// 26// v1 is the COMPOSITION skeleton. Bitrate optimisation (entropy 27// coding of residual via nx_range_coder, codebook-VQ of LPC) is the 28// v2 path documented in NISHI_COMMS_ROADMAP.md. The point of v1 is 29// to prove every substrate primitive composes end-to-end with no 30// external dependency. 31 32// nx_safety_envelope: 33// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 34// sil_target: SIL1 35// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 36// verdict: NOT_YET_EVALUATED 37 38import "nx_syscalls.nx" 39import "nx_voice_frame.nx" 40import "nx_lpc.nx" 41import "nx_vad.nx" 42import "nx_voice_fec.nx" 43import "nx_voice_codec_tier.nx" 44 45// Sealed verdict per encode/decode operation. 46const NX_VC_VERDICT_UNKNOWN: i64 = 0 47const NX_VC_VERDICT_OK: i64 = 1 48const NX_VC_VERDICT_BUF_TOO_SMALL: i64 = 2 49const NX_VC_VERDICT_BAD_PAYLOAD: i64 = 3 50const NX_VC_VERDICT_FRAME_REJECTED: i64 = 4 51const NX_VC_VERDICT_BAD_PARAMS: i64 = 5 52const NX_VC_VERDICT_N: i64 = 6 53 54// Maximum LPC order we support in v1. Tier-driven; T0 uses 6, T4 uses 16. 55const NX_VC_MAX_LPC_ORDER: i64 = 20 56// Number of evenly-spaced residual subsamples carried in payload. 57// Higher = better fidelity / higher bitrate. 8 is the v1 default. 58const NX_VC_RES_SUBSAMPLES: i64 = 8 59 60// Codec persistent state. Caller pre-allocates each buffer and 61// passes them to nx_voice_codec_init -- substrate cardinal: no hidden 62// allocation; every byte is auditable. 63struct VoiceCodecState { 64 params: *VoiceCodecParams, // caller-owned 65 vad: *VadState, // caller-owned, size >= 64 66 fec: *FecState, // caller-owned, size >= 64 67 fec_last_buf: *u8, // FEC last-frame mirror, cap >= 256 68 fec_last_cap: i64, 69 prev_hash: *u8, // 32-byte chain mirror 70 sequence: i64, 71 // LPC scratch (4 buffers, each holding (order+1) i64). 72 R: *i64, 73 a_q15: *i64, 74 refl_q15: *i64, 75 a_tmp: *i64, 76 init_done: i64 77} 78 79// Initialise the codec. Caller has already filled `params` (via 80// nx_voice_codec_init from nx_voice_codec_tier.nx) and allocated 81// every buffer. This zeroes the chain hash + seq, primes VAD/FEC. 82func nx_voice_codec_state_init( 83 state: *VoiceCodecState, 84 params: *VoiceCodecParams, 85 vad: *VadState, fec: *FecState, 86 fec_last_buf: *u8, fec_last_cap: i64, 87 prev_hash: *u8, 88 R: *i64, a_q15: *i64, refl_q15: *i64, a_tmp: *i64 89) -> i64 { 90 state.params = params 91 state.vad = vad 92 state.fec = fec 93 state.fec_last_buf = fec_last_buf 94 state.fec_last_cap = fec_last_cap 95 state.prev_hash = prev_hash 96 state.sequence = 0 97 state.R = R 98 state.a_q15 = a_q15 99 state.refl_q15 = refl_q15 100 state.a_tmp = a_tmp 101 nx_vad_init(vad) 102 nx_fec_init(fec, fec_last_buf, fec_last_cap) 103 var i: i64 = 0 104 while i < 32 { prev_hash[i] = 0; i = i + 1 } 105 state.init_done = 1 106 return NX_VC_VERDICT_OK 107} 108 109// Pack int16 little-endian into payload[off]. Returns off+2. 110func _w16(payload: *u8, off: i64, v: i64) -> i64 { 111 payload[off] = v & 0xff 112 payload[off + 1] = (v >> 8) & 0xff 113 return off + 2 114} 115 116// Read int16 little-endian sign-extended. 117func _r16(payload: *u8, off: i64) -> i64 { 118 var v: i64 = payload[off] | (payload[off + 1] << 8) 119 if v >= 32768 { v = v - 65536 } 120 return v 121} 122 123// Encode one frame of PCM samples (already in i64, signed, Q-scale). 124// Returns total bytes written to out_buf (header + payload + FEC), or 125// a sealed NX_VC_VERDICT_* on failure (caller checks <= NX_VC_VERDICT_N). 126// 127// `samples` is `n` ints; `out_buf` capacity must be >= 256. 128func nx_voice_codec_encode( 129 state: *VoiceCodecState, 130 samples: *i64, n: i64, 131 out_buf: *u8, out_cap: i64, 132 out_total_len: *i64 133) -> i64 { 134 if state.init_done != 1 { return NX_VC_VERDICT_BAD_PARAMS } 135 if out_cap < 128 { return NX_VC_VERDICT_BUF_TOO_SMALL } 136 137 let order: i64 = state.params.lpc_order 138 if order < 1 { return NX_VC_VERDICT_BAD_PARAMS } 139 if order > NX_VC_MAX_LPC_ORDER { return NX_VC_VERDICT_BAD_PARAMS } 140 141 let vad_verdict: i64 = nx_vad_classify(state.vad, samples, n) 142 143 // Build payload into a temp area inside out_buf (after frame header). 144 let payload_buf: *u8 = (out_buf as i64 + NX_VFR_HEADER_BYTES) as *u8 145 let payload_cap: i64 = out_cap - NX_VFR_HEADER_BYTES 146 var pl: i64 = 0 147 if payload_cap < 8 { return NX_VC_VERDICT_BUF_TOO_SMALL } 148 149 // Byte 0: VAD verdict. 150 payload_buf[pl] = vad_verdict & 0xff 151 pl = pl + 1 152 153 if vad_verdict == NX_VAD_VERDICT_SILENCE { 154 // Silence frame: 1 byte energy + 1 byte cn-seed. ~13 byte total 155 // frame payload (vs 32+ for voice). Massive bandwidth saving. 156 let e: i64 = nx_vad_energy(samples, n) 157 var e_byte: i64 = 0 158 var ee: i64 = e 159 while ee > 0 { e_byte = e_byte + 1; ee = ee >> 1 } 160 payload_buf[pl] = e_byte & 0xff 161 pl = pl + 1 162 payload_buf[pl] = state.sequence & 0xff 163 pl = pl + 1 164 } else { 165 // Voice / noise / transition: full LPC + subsamples. 166 let lpc_verdict: i64 = nx_lpc_analyze( 167 samples, n, order, 168 state.R, state.a_q15, state.refl_q15, state.a_tmp) 169 // Order byte. 170 if pl + 1 > payload_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 171 payload_buf[pl] = order & 0xff 172 pl = pl + 1 173 // LPC verdict byte (so decoder can see if encoder fell back). 174 if pl + 1 > payload_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 175 payload_buf[pl] = lpc_verdict & 0xff 176 pl = pl + 1 177 // LPC coefficients a_q15[1..order] as int16 little-endian. 178 var i: i64 = 1 179 while i <= order { 180 if pl + 2 > payload_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 181 pl = _w16(payload_buf, pl, state.a_q15[i]) 182 i = i + 1 183 } 184 // Subsample count byte. 185 if pl + 1 > payload_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 186 payload_buf[pl] = NX_VC_RES_SUBSAMPLES & 0xff 187 pl = pl + 1 188 // 8 evenly-spaced subsamples as int16. At n=160 (20ms@8kHz) 189 // that's stride 20. 190 var stride: i64 = n / NX_VC_RES_SUBSAMPLES 191 if stride < 1 { stride = 1 } 192 var k: i64 = 0 193 while k < NX_VC_RES_SUBSAMPLES { 194 if pl + 2 > payload_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 195 var idx: i64 = k * stride 196 if idx >= n { idx = n - 1 } 197 var s: i64 = samples[idx] 198 // Clip to int16 range. 199 if s > 32767 { s = 32767 } 200 if s < -32768 { s = -32768 } 201 if s < 0 { s = s + 65536 } 202 pl = _w16(payload_buf, pl, s) 203 k = k + 1 204 } 205 } 206 207 // Build the wire frame with chain header. 208 let flags: i64 = NX_VFR_FLAG_VOICE | NX_VFR_FLAG_FEC 209 let total_main: i64 = nx_voice_frame_build( 210 out_buf, out_cap, 211 state.params.tier_used, 212 state.params.bitrate_kbps, 213 state.params.sample_rate_khz, 214 state.params.frame_ms, 215 flags, 216 state.sequence, 217 state.prev_hash, 218 payload_buf, pl) 219 if total_main < 0 { return NX_VC_VERDICT_BUF_TOO_SMALL } 220 221 // Append FEC block after main frame (in same buffer). 222 var fec_len: i64 = 0 223 if out_cap > total_main + 8 { 224 let fec_out: *u8 = (out_buf as i64 + total_main) as *u8 225 fec_len = nx_fec_encode_block( 226 state.fec, 227 payload_buf, pl, 228 fec_out, out_cap - total_main) 229 if fec_len < 0 { fec_len = 0 } 230 } 231 232 nx_fec_remember(state.fec, payload_buf, pl, state.sequence) 233 234 // Update chain: hash this frame's bytes into prev_hash for next call. 235 // We re-use nx_voice_frame_parse's hash field by computing it inline. 236 // Cheap path: zero prev_hash for v1; the parser already chains in 237 // its own output. Refinement queued. 238 state.sequence = state.sequence + 1 239 240 *out_total_len = total_main + fec_len 241 return NX_VC_VERDICT_OK 242} 243 244// Decode one frame. `in_buf`/`in_len` is one wire frame (header + 245// payload, FEC trailer optional). Writes reconstructed samples to 246// `out_samples` (i64 little-endian Q-scale), returns n samples 247// written via *out_n. 248func nx_voice_codec_decode( 249 state: *VoiceCodecState, 250 in_buf: *u8, in_len: i64, 251 out_samples: *i64, out_cap: i64, 252 out_n: *i64 253) -> i64 { 254 if state.init_done != 1 { return NX_VC_VERDICT_BAD_PARAMS } 255 256 // Parse the frame (don't enforce chain on first decode). 257 let off_slot: *i64 = sys_mmap(16) as *i64 258 let len_slot: *i64 = sys_mmap(16) as *i64 259 let hash_slot: *u8 = sys_mmap(64) 260 let v: i64 = nx_voice_frame_parse( 261 in_buf, in_len, 262 0 as *u8, // skip chain check for v1 263 off_slot, len_slot, 264 hash_slot) 265 if v != NX_VFR_VERDICT_OK { return NX_VC_VERDICT_FRAME_REJECTED } 266 267 let payload_off: i64 = *off_slot 268 let payload_len: i64 = *len_slot 269 let payload: *u8 = (in_buf as i64 + payload_off) as *u8 270 if payload_len < 1 { return NX_VC_VERDICT_BAD_PAYLOAD } 271 272 let vad_byte: i64 = payload[0] 273 var pl_pos: i64 = 1 274 let frame_n: i64 = (state.params.sample_rate_khz * state.params.frame_ms) 275 276 if vad_byte == NX_VAD_VERDICT_SILENCE { 277 // Comfort noise: emit zero samples for v1. 278 var i: i64 = 0 279 while i < frame_n { 280 if i >= out_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 281 out_samples[i] = 0 282 i = i + 1 283 } 284 *out_n = frame_n 285 return NX_VC_VERDICT_OK 286 } 287 288 // Voice: read order + verdict + coeffs + subsamples. 289 if pl_pos + 2 > payload_len { return NX_VC_VERDICT_BAD_PAYLOAD } 290 let order: i64 = payload[pl_pos]; pl_pos = pl_pos + 1 291 let lpc_verdict_byte: i64 = payload[pl_pos]; pl_pos = pl_pos + 1 292 if order < 1 { return NX_VC_VERDICT_BAD_PAYLOAD } 293 if order > NX_VC_MAX_LPC_ORDER { return NX_VC_VERDICT_BAD_PAYLOAD } 294 if pl_pos + order * 2 > payload_len { return NX_VC_VERDICT_BAD_PAYLOAD } 295 296 var i: i64 = 1 297 while i <= order { 298 state.a_q15[i] = _r16(payload, pl_pos) 299 pl_pos = pl_pos + 2 300 i = i + 1 301 } 302 if pl_pos + 1 > payload_len { return NX_VC_VERDICT_BAD_PAYLOAD } 303 let n_sub: i64 = payload[pl_pos]; pl_pos = pl_pos + 1 304 if n_sub > NX_VC_RES_SUBSAMPLES { return NX_VC_VERDICT_BAD_PAYLOAD } 305 if pl_pos + n_sub * 2 > payload_len { return NX_VC_VERDICT_BAD_PAYLOAD } 306 307 // Read subsamples. 308 let sub_buf: *i64 = sys_mmap(NX_VC_RES_SUBSAMPLES * 8) as *i64 309 var k: i64 = 0 310 while k < n_sub { 311 sub_buf[k] = _r16(payload, pl_pos) 312 pl_pos = pl_pos + 2 313 k = k + 1 314 } 315 316 // Piecewise linear expansion of subsamples to a full frame. 317 // Production v2 will run the LPC inverse filter with a decoded 318 // residual instead. v1 proves the composition. 319 var stride: i64 = frame_n / n_sub 320 if stride < 1 { stride = 1 } 321 var out_i: i64 = 0 322 var seg: i64 = 0 323 while seg < n_sub { 324 let s0: i64 = sub_buf[seg] 325 var s1: i64 = s0 326 if seg + 1 < n_sub { s1 = sub_buf[seg + 1] } 327 var p: i64 = 0 328 while p < stride { 329 if out_i >= out_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 330 let interp: i64 = s0 + ((s1 - s0) * p) / stride 331 out_samples[out_i] = interp 332 out_i = out_i + 1 333 p = p + 1 334 } 335 seg = seg + 1 336 } 337 // Pad if rounding left a gap. 338 while out_i < frame_n { 339 if out_i >= out_cap { return NX_VC_VERDICT_BUF_TOO_SMALL } 340 out_samples[out_i] = sub_buf[n_sub - 1] 341 out_i = out_i + 1 342 } 343 344 *out_n = out_i 345 return NX_VC_VERDICT_OK 346} 347 348// Sealed-enum validity gate. 349func nx_vc_verdict_is_valid(v: i64) -> i64 { 350 if v < 0 { return 0 } 351 if v >= NX_VC_VERDICT_N { return 0 } 352 return 1 353}