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}