nx_voice_codec_v1_adapter.nx source
↩ module page · 230 lines · 9274 B
1// nx_voice_codec_v1_adapter.nx -- wraps NishiVoice v1 behind nx_codec.
2//
3// NishiVoice v1's native API takes `*i64` PCM-sample arrays + writes
4// total-length-via-out-param. The OOP layer's nx_codec vtable speaks
5// `*u8` byte buffers + returns-bytes-written. This adapter is the
6// thin shim: it unpacks int16-LE PCM bytes into i64 samples, calls
7// nx_voice_codec_encode, repacks decoded i64 samples back to int16-LE
8// PCM bytes on the way out.
9//
10// Why this exists: nx_audio_session works against the nx_codec
11// interface; the smoke `bench/nx_audio_session_smoke.nx` proved the
12// plumbing with a passthrough codec. This file replaces passthrough
13// with the REAL NishiVoice v1 substrate (~1850 LOC of LPC + VAD +
14// FEC + frame format) without touching the session layer.
15//
16// Composes:
17// nx_codec.nx -- abstract base
18// nx_voice_codec.nx -- NishiVoice v1 encode + decode
19// nx_voice_codec_tier.nx -- params per hw tier
20//
21// Per [[feedback-bits-up-exceed-never-match]]: zero new codec logic.
22// The wrapper is glue, not invention. All audio quality wins/losses
23// vs Opus belong to the underlying nx_voice_codec.
24
25// nx_safety_envelope:
26// intended_use: glue between nx_codec OOP interface and NishiVoice v1
27// sil_target: SIL2
28// evidence: voice_call_test.sh proves NishiVoice v1 wire round-
29// trips between two real Linux processes; this adapter
30// preserves that property by faithfully forwarding
31// through the codec's native API
32// verdict: NOT_YET_EVALUATED -- pending adapter smoke
33
34import "nx_syscalls.nx"
35import "nx_codec.nx"
36import "nx_voice_codec.nx"
37import "nx_voice_codec_tier.nx"
38import "nx_hwprobe.nx"
39
40// ---- Sealed scratch sizes (chosen for max LPC order = 20) ----
41
42const NX_VC1A_VAD_BYTES: i64 = 64
43const NX_VC1A_FEC_BYTES: i64 = 64
44const NX_VC1A_FEC_LAST_CAP: i64 = 256
45const NX_VC1A_PREV_HASH: i64 = 32
46const NX_VC1A_LPC_SCRATCH: i64 = 168 // (NX_VC_MAX_LPC_ORDER+1) * 8
47
48// ---- Adapter state ----
49//
50// Held in c.state (cast to *nx_voice_codec_v1_state). All buffers
51// owned by the adapter (allocated in _new); no caller-provided
52// scratch.
53
54struct nx_voice_codec_v1_state {
55 params: *VoiceCodecParams,
56 vc_state: *VoiceCodecState,
57 vad: *VadState,
58 fec: *FecState,
59 fec_last_buf: *u8,
60 prev_hash: *u8,
61 R: *i64,
62 a_q15: *i64,
63 refl_q15: *i64,
64 a_tmp: *i64,
65 // Scratch for sample-conversion (avoid alloc per frame).
66 sample_scratch: *i64,
67 sample_scratch_cap: i64,
68 out_n_slot: *i64,
69}
70
71const NX_VC1A_STATE_BYTES: i64 = 104
72
73// ---- Vtable impls ----
74
75// Unpack int16-LE bytes -> i64 sample array. Returns sample count.
76func _unpack_pcm_int16le(src: *u8, src_n: i64, dst: *i64, dst_cap: i64) -> i64 {
77 let n_samples: i64 = src_n / 2
78 if n_samples > dst_cap { return -1 }
79 var i: i64 = 0
80 while i < n_samples {
81 var v: i64 = src[i * 2] | (src[i * 2 + 1] << 8)
82 if v >= 32768 { v = v - 65536 } // sign-extend int16
83 dst[i] = v
84 i = i + 1
85 }
86 return n_samples
87}
88
89// Pack i64 samples -> int16-LE bytes. Returns byte count.
90// Clips out-of-int16-range samples to fit.
91func _pack_pcm_int16le(src: *i64, n_samples: i64, dst: *u8, dst_cap: i64) -> i64 {
92 let need: i64 = n_samples * 2
93 if need > dst_cap { return -1 }
94 var i: i64 = 0
95 while i < n_samples {
96 var v: i64 = src[i]
97 if v > 32767 { v = 32767 }
98 if v < -32768 { v = -32768 }
99 if v < 0 { v = v + 65536 }
100 dst[i * 2] = (v & 0xff) as u8
101 dst[i * 2 + 1] = ((v >> 8) & 0xff) as u8
102 i = i + 1
103 }
104 return need
105}
106
107func nx_voice_codec_v1_encode(c: *nx_codec, src: *u8, src_n: i64,
108 dst: *u8, dst_cap: i64) -> i64 {
109 let st: *nx_voice_codec_v1_state = c.state as *nx_voice_codec_v1_state
110
111 // Unpack int16-LE PCM bytes into i64 sample array.
112 let n_samples: i64 = _unpack_pcm_int16le(src, src_n,
113 st.sample_scratch,
114 st.sample_scratch_cap)
115 if n_samples < 0 { return NX_CODEC_ERR_BAD_INPUT }
116
117 // Call the real codec. out_n_slot is owned by st; extract to
118 // local first because parser can't *st.field = expr write
119 // through a dereferenced field access.
120 let out_n_slot: *i64 = st.out_n_slot
121 out_n_slot[0] = 0
122 let v: i64 = nx_voice_codec_encode(st.vc_state,
123 st.sample_scratch, n_samples,
124 dst, dst_cap,
125 out_n_slot)
126 if v != NX_VC_VERDICT_OK {
127 if v == NX_VC_VERDICT_BUF_TOO_SMALL { return NX_CODEC_ERR_OUT_TOO_SMALL }
128 if v == NX_VC_VERDICT_BAD_PARAMS { return NX_CODEC_ERR_BAD_INPUT }
129 return NX_CODEC_ERR_BAD_INPUT
130 }
131 return out_n_slot[0]
132}
133
134func nx_voice_codec_v1_decode(c: *nx_codec, src: *u8, src_n: i64,
135 dst: *u8, dst_cap: i64) -> i64 {
136 let st: *nx_voice_codec_v1_state = c.state as *nx_voice_codec_v1_state
137
138 // Decode into the i64 sample scratch.
139 let out_n_slot: *i64 = st.out_n_slot
140 out_n_slot[0] = 0
141 let v: i64 = nx_voice_codec_decode(st.vc_state,
142 src, src_n,
143 st.sample_scratch, st.sample_scratch_cap,
144 out_n_slot)
145 if v != NX_VC_VERDICT_OK {
146 if v == NX_VC_VERDICT_BUF_TOO_SMALL { return NX_CODEC_ERR_OUT_TOO_SMALL }
147 if v == NX_VC_VERDICT_FRAME_REJECTED { return NX_CODEC_ERR_BAD_MAGIC }
148 if v == NX_VC_VERDICT_BAD_PAYLOAD { return NX_CODEC_ERR_BAD_INPUT }
149 return NX_CODEC_ERR_BAD_INPUT
150 }
151
152 // Repack i64 samples -> int16-LE bytes.
153 return _pack_pcm_int16le(st.sample_scratch, out_n_slot[0], dst, dst_cap)
154}
155
156// frame_cas_hash and grade_card not implemented in v1 of the adapter;
157// fall back to default error-returning vtable entries. Future work
158// queues these per [[feedback-no-false-ok-substrate-honesty-audit]].
159
160// ---- Constructor ----
161//
162// Allocates ALL backing buffers, initialises the underlying
163// VoiceCodecState, wires the nx_codec vtable. Caller passes in the
164// pre-allocated *nx_codec; the adapter owns everything below.
165
166func nx_voice_codec_v1_new(c: *nx_codec) -> i64 {
167 // Pick params for this machine's hw tier.
168 let params: *VoiceCodecParams = sys_mmap(64) as *VoiceCodecParams
169 nx_voice_codec_init(params)
170
171 // Derive frame size in samples = sample_rate_khz * frame_ms.
172 let frame_samples: i64 = params.sample_rate_khz * params.frame_ms
173
174 // Allocate all the scratch buffers.
175 let vad: *VadState = sys_mmap(NX_VC1A_VAD_BYTES) as *VadState
176 let fec: *FecState = sys_mmap(NX_VC1A_FEC_BYTES) as *FecState
177 let fec_last_buf: *u8 = sys_mmap(NX_VC1A_FEC_LAST_CAP)
178 let prev_hash: *u8 = sys_mmap(NX_VC1A_PREV_HASH)
179 let R: *i64 = sys_mmap(NX_VC1A_LPC_SCRATCH) as *i64
180 let a_q15: *i64 = sys_mmap(NX_VC1A_LPC_SCRATCH) as *i64
181 let refl_q15: *i64 = sys_mmap(NX_VC1A_LPC_SCRATCH) as *i64
182 let a_tmp: *i64 = sys_mmap(NX_VC1A_LPC_SCRATCH) as *i64
183
184 let vc_state: *VoiceCodecState = sys_mmap(128) as *VoiceCodecState
185 nx_voice_codec_state_init(vc_state, params, vad, fec,
186 fec_last_buf, NX_VC1A_FEC_LAST_CAP,
187 prev_hash,
188 R, a_q15, refl_q15, a_tmp)
189
190 // Per-frame i64 sample scratch (size 4x frame for safety -- handles
191 // any reasonable tier without realloc).
192 let scratch_n: i64 = frame_samples * 4
193 let sample_scratch: *i64 = sys_mmap(scratch_n * 8) as *i64
194 let out_n_slot: *i64 = sys_mmap(16) as *i64
195
196 // Build adapter state.
197 let st: *nx_voice_codec_v1_state = sys_mmap(NX_VC1A_STATE_BYTES)
198 as *nx_voice_codec_v1_state
199 st.params = params
200 st.vc_state = vc_state
201 st.vad = vad
202 st.fec = fec
203 st.fec_last_buf = fec_last_buf
204 st.prev_hash = prev_hash
205 st.R = R
206 st.a_q15 = a_q15
207 st.refl_q15 = refl_q15
208 st.a_tmp = a_tmp
209 st.sample_scratch = sample_scratch
210 st.sample_scratch_cap = scratch_n
211 st.out_n_slot = out_n_slot
212
213 // Initialise the nx_codec base.
214 nx_codec_init(c,
215 NX_CODEC_ID_NISHIVOICE_V1,
216 NX_CODEC_KIND_AUDIO,
217 frame_samples * 2, // frame_size_in (int16 bytes per frame)
218 256, // frame_size_out (max wire bytes; matches nx_voice_codec internal cap)
219 params.sample_rate_khz * 1000, // sample_rate (Hz)
220 params.bitrate_kbps * 1000, // bitrate_target (bps)
221 params.tier_used) // hw tier
222
223 c.state = st as i64
224 c.encode_fn = nx_voice_codec_v1_encode
225 c.decode_fn = nx_voice_codec_v1_decode
226 // frame_cas_hash_fn + grade_card_fn left as defaults (return NOT_SUPPORTED)
227 // until adapter v2 wires them.
228
229 return 0
230}