code wiki / (root) / nx_voice_codec_v1_adapter.nx

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}