nx_lpc_levinson.nx source
↩ module page · 302 lines · 12408 B
1// nx_lpc_levinson.nx -- Levinson-Durbin recursion in Q30 fixed-point.
2// Arc 1 Phase A.2 (after nx_lpc_autocorr).
3//
4// Given autocorrelation R[0..p] (i64 from nx_lpc_autocorr), computes:
5// - reflection coefficients (PARCOR) k[1..p] in Q30 (|k_i| < 1 for stable)
6// - LPC predictor coefficients a[1..p] in Q30
7// - prediction error E[p] in i64 (= R[0] * Π(1 - k_i²))
8//
9// Re-derived from Levinson-Durbin / Yule-Walker. Patent-clean math.
10// Per [[feedback-bits-up-exceed-never-match]] — no Opus, no MLow,
11// no proprietary codec dependency, integer-only.
12//
13// Algorithm (per any DSP textbook, e.g. Rabiner & Schafer 1978 §8.3):
14// E = R[0]
15// for i = 1 to p:
16// num = -R[i] - Σ_{j=1..i-1} a[j] * R[i-j] (with proper scaling)
17// k_i = num / E
18// new_a[i] = k_i
19// for j = 1 to i-1: new_a[j] = a[j] + k_i * a[i-j]
20// E = E * (1 - k_i²)
21// a = new_a
22//
23// Fixed-point convention:
24// - R[*] in raw i64 (autocorrelation sums)
25// - E in raw i64 (always positive, decreasing each iteration)
26// - k[*] in Q30: real_k = stored_k / 2^30, |real_k| < 1 ⇒ |stored_k| < 2^30
27// - a[*] in Q30: same convention; final coefs bounded by O(2^p) typically
28//
29// Returns 0 on success, -1 if R[0] <= 0 (degenerate / silence) or
30// p > 30 (would overflow Q30 in a coefs).
31
32import "nx_syscalls_x86_64.nx"
33const NX_MAGIC_1073741823: i64 = 1073741823
34const NX_MAGIC_1024: i64 = 1024
35
36const NX_LPC_Q: i64 = 30
37const NX_LPC_ONE_Q30: i64 = 1073741824 // 1 << 30
38
39// Helper: i64 Q30 multiplication. (a * b) >> 30 with i64 intermediate.
40// Caller's responsibility to ensure |a| * |b| fits in i64 before shift,
41// which is guaranteed when both are Q30 values with |a|, |b| < 2^31.
42func _q30_mul(a: i64, b: i64) -> i64 {
43 return (a * b) >> NX_LPC_Q
44}
45
46// Helper: i64 division a / b, where b > 0, result is signed.
47// NishiLang's `/` should do truncated-toward-zero integer division for i64.
48func _i64_div(a: i64, b: i64) -> i64 {
49 return a / b
50}
51
52// nx_lpc_levinson -- p must be in [1, 30].
53//
54// r_in: i64 LE array R[0..p] ((p+1) * 8 bytes)
55// p_order: 1..30
56// k_out: i64 LE array k[0..p-1] (Q30) -- k_out[i-1] = k_i
57// a_out: i64 LE array a[0..p-1] (Q30) -- a_out[i-1] = a_i
58// e_out: single i64 LE = E[p] (raw, same units as R[0])
59//
60// Returns 0 on success.
61func nx_lpc_levinson(r_in: *u8, p_order: i64,
62 k_out: *u8, a_out: *u8, e_out: *u8) -> i64 {
63 if p_order < 1 { return -1 }
64 if p_order > 30 { return -1 }
65
66 // R[0] must be > 0. Otherwise the signal is silent / degenerate.
67 let r0_off: i64 = 0
68 var r0: i64 = 0
69 var bi: i64 = 0
70 while bi < 8 {
71 let bb: i64 = r_in[r0_off + bi]
72 r0 = r0 | (bb << (bi * 8))
73 bi = bi + 1
74 }
75 if r0 <= 0 { return -1 }
76
77 // Normalize R[0..p] so R[0] fits in [2^28, 2^31]. Without this,
78 // large-magnitude inputs (real speech R[0] ~ 2^33+) cause acc<<30
79 // to overflow + the e_cur>>30 fallback path divides by zero once
80 // e_cur shrinks below 2^30. Apply the same shift uniformly to
81 // ALL R[k] -- k and a coefficients are dimensionless ratios so
82 // they are unaffected. Only E[p] picks up the scale; we restore
83 // it by left-shifting at the end.
84 var norm_shift: i64 = 0
85 var r0_test: i64 = r0
86 while r0_test > 0x80000000 { // > 2^31, shift down
87 norm_shift = norm_shift + 1
88 r0_test = r0_test >> 1
89 }
90 // Apply norm_shift to ALL R[k] in place by writing back to r_in
91 // (allowed: caller's R buffer is consumed during the recursion).
92 if norm_shift > 0 {
93 var nk: i64 = 0
94 while nk <= p_order {
95 let nk_off: i64 = nk * 8
96 var rk: i64 = 0
97 var nbi: i64 = 0
98 while nbi < 8 {
99 rk = rk | ((r_in[nk_off + nbi]) << (nbi * 8))
100 nbi = nbi + 1
101 }
102 let rk_shifted: i64 = rk >> norm_shift
103 r_in[nk_off + 0] = rk_shifted & 0xff
104 r_in[nk_off + 1] = (rk_shifted >> 8) & 0xff
105 r_in[nk_off + 2] = (rk_shifted >> 16) & 0xff
106 r_in[nk_off + 3] = (rk_shifted >> 24) & 0xff
107 r_in[nk_off + 4] = (rk_shifted >> 32) & 0xff
108 r_in[nk_off + 5] = (rk_shifted >> 40) & 0xff
109 r_in[nk_off + 6] = (rk_shifted >> 48) & 0xff
110 r_in[nk_off + 7] = (rk_shifted >> 56) & 0xff
111 nk = nk + 1
112 }
113 r0 = r0 >> norm_shift
114 }
115
116 // Working buffers: a[1..p] and new_a[1..p] in Q30.
117 // Use sys_mmap for transient storage (32 * 8 = 256 bytes each).
118 let a_cur: *u8 = sys_mmap(256)
119 let a_new: *u8 = sys_mmap(256)
120 // Zero both.
121 var zi: i64 = 0
122 while zi < 32 {
123 let off: i64 = zi * 8
124 a_cur[off] = 0; a_cur[off+1] = 0; a_cur[off+2] = 0; a_cur[off+3] = 0
125 a_cur[off+4] = 0; a_cur[off+5] = 0; a_cur[off+6] = 0; a_cur[off+7] = 0
126 a_new[off] = 0; a_new[off+1] = 0; a_new[off+2] = 0; a_new[off+3] = 0
127 a_new[off+4] = 0; a_new[off+5] = 0; a_new[off+6] = 0; a_new[off+7] = 0
128 zi = zi + 1
129 }
130
131 var e_cur: i64 = r0
132
133 // Main recursion: i = 1..p
134 var i: i64 = 1
135 while i <= p_order {
136 // num = -R[i] - Σ_{j=1..i-1} a[j] * R[i-j]
137 // R values are raw i64; a values are Q30. So a[j] * R[i-j] is
138 // Q30 * raw = needs >> 30 to bring back to raw scale.
139 let r_i_off: i64 = i * 8
140 var r_i: i64 = 0
141 var ri_bi: i64 = 0
142 while ri_bi < 8 {
143 r_i = r_i | ((r_in[r_i_off + ri_bi]) << (ri_bi * 8))
144 ri_bi = ri_bi + 1
145 }
146 // Sign-extend r_i if top bit set (already i64 LE, so it's fine)
147
148 var acc: i64 = 0 - r_i
149 var j: i64 = 1
150 while j < i {
151 let aj_off: i64 = (j - 1) * 8
152 var a_j: i64 = 0
153 var aj_bi: i64 = 0
154 while aj_bi < 8 {
155 a_j = a_j | ((a_cur[aj_off + aj_bi]) << (aj_bi * 8))
156 aj_bi = aj_bi + 1
157 }
158 let r_imj_off: i64 = (i - j) * 8
159 var r_imj: i64 = 0
160 var rimj_bi: i64 = 0
161 while rimj_bi < 8 {
162 r_imj = r_imj | ((r_in[r_imj_off + rimj_bi]) << (rimj_bi * 8))
163 rimj_bi = rimj_bi + 1
164 }
165 // a[j] * R[i-j]: Q30 * raw, sum into acc (raw scale).
166 // Bring a[j] to raw by >> 30 after multiply.
167 acc = acc - ((a_j * r_imj) >> NX_LPC_Q)
168 j = j + 1
169 }
170 // k_i = acc / E_{i-1}, in Q30: (acc << 30) / E
171 // To avoid overflow: |acc| << 30 may exceed i64. Use mul-shift trick:
172 // k_i (Q30) = (acc / E) << 30 + ((acc % E) << 30) / E -- still tricky
173 // Simpler: divide first, then shift, accepting precision loss for now.
174 // OR: shift acc up by 30 if |acc| < 2^33 (which it is for normal signals).
175 // For test patterns we expect |acc| << 2^33, so the shift-first path is fine.
176 // S-class Q30 division: compute (acc << 30) / e_cur EXACTLY
177 // without i128, using the identity
178 // (acc << 30) / e = (acc / e) << 30 + ((acc % e) << 30) / e
179 // First term: (acc / e) is small after normalization (e_cur was
180 // at most 2^31 initially, shrinks each iteration; acc bounded
181 // by similar magnitude; quotient fits well in i32 typically).
182 // Left-shift by 30 fits in i64 when |acc/e| < 2^33.
183 // Second term: (acc % e) is in [0, |e|); (acc % e) << 30 fits
184 // in i64 when |e| < 2^33 (always true post-normalization).
185 // Sign handled by splitting on acc's sign and using positive
186 // |acc| in the formula.
187 var k_i: i64 = 0
188 var pos_acc: i64 = acc
189 var neg: i64 = 0
190 if acc < 0 { pos_acc = 0 - acc; neg = 1 }
191 let q_high: i64 = (pos_acc / e_cur) << NX_LPC_Q
192 let q_low: i64 = ((pos_acc % e_cur) << NX_LPC_Q) / e_cur
193 k_i = q_high + q_low
194 if neg == 1 { k_i = 0 - k_i }
195 // Clamp |k_i| < 2^30 for stable LPC (|reflection coef| < 1).
196 // Outside this range, system is unstable; clamp to ±(1 << 30 - 1).
197 if k_i > NX_MAGIC_1073741823 { k_i = NX_MAGIC_1073741823 }
198 if k_i < -NX_MAGIC_1073741823 { k_i = -NX_MAGIC_1073741823 }
199
200 // Store k[i-1] = k_i (Q30) to k_out.
201 let ki_off: i64 = (i - 1) * 8
202 var v: i64 = k_i
203 if v < 0 { v = v + 0x10000000000000000 }
204 // store low 8 bytes of v as LE; signed top bit comes out correct
205 k_out[ki_off + 0] = k_i & 0xff
206 k_out[ki_off + 1] = (k_i >> 8) & 0xff
207 k_out[ki_off + 2] = (k_i >> 16) & 0xff
208 k_out[ki_off + 3] = (k_i >> 24) & 0xff
209 k_out[ki_off + 4] = (k_i >> 32) & 0xff
210 k_out[ki_off + 5] = (k_i >> 40) & 0xff
211 k_out[ki_off + 6] = (k_i >> 48) & 0xff
212 k_out[ki_off + 7] = (k_i >> 56) & 0xff
213
214 // new_a[i] = k_i
215 let new_ai_off: i64 = (i - 1) * 8
216 a_new[new_ai_off + 0] = k_i & 0xff
217 a_new[new_ai_off + 1] = (k_i >> 8) & 0xff
218 a_new[new_ai_off + 2] = (k_i >> 16) & 0xff
219 a_new[new_ai_off + 3] = (k_i >> 24) & 0xff
220 a_new[new_ai_off + 4] = (k_i >> 32) & 0xff
221 a_new[new_ai_off + 5] = (k_i >> 40) & 0xff
222 a_new[new_ai_off + 6] = (k_i >> 48) & 0xff
223 a_new[new_ai_off + 7] = (k_i >> 56) & 0xff
224
225 // For j = 1..i-1: new_a[j] = a[j] + k_i * a[i-j]
226 var jj: i64 = 1
227 while jj < i {
228 let aj_off: i64 = (jj - 1) * 8
229 var a_j: i64 = 0
230 var aj_b: i64 = 0
231 while aj_b < 8 {
232 a_j = a_j | ((a_cur[aj_off + aj_b]) << (aj_b * 8))
233 aj_b = aj_b + 1
234 }
235 let aimj_off: i64 = (i - jj - 1) * 8
236 var a_imj: i64 = 0
237 var aimj_b: i64 = 0
238 while aimj_b < 8 {
239 a_imj = a_imj | ((a_cur[aimj_off + aimj_b]) << (aimj_b * 8))
240 aimj_b = aimj_b + 1
241 }
242 let new_aj: i64 = a_j + _q30_mul(k_i, a_imj)
243 let new_aj_off: i64 = (jj - 1) * 8
244 a_new[new_aj_off + 0] = new_aj & 0xff
245 a_new[new_aj_off + 1] = (new_aj >> 8) & 0xff
246 a_new[new_aj_off + 2] = (new_aj >> 16) & 0xff
247 a_new[new_aj_off + 3] = (new_aj >> 24) & 0xff
248 a_new[new_aj_off + 4] = (new_aj >> 32) & 0xff
249 a_new[new_aj_off + 5] = (new_aj >> 40) & 0xff
250 a_new[new_aj_off + 6] = (new_aj >> 48) & 0xff
251 a_new[new_aj_off + 7] = (new_aj >> 56) & 0xff
252 jj = jj + 1
253 }
254
255 // E = E * (1 - k_i²)
256 let k2: i64 = (k_i * k_i) >> NX_LPC_Q // Q30
257 let one_minus_k2: i64 = NX_LPC_ONE_Q30 - k2
258 // S-class graceful early-termination: when |k_i| → 1 (perfect AR
259 // fit at current order), the signal has no further LPC structure.
260 // Stop the recursion; remaining k[i+1..p] and a[i+1..p] stay 0
261 // (already zeroed in init). Per [[feedback-build-intelligence-never-strip-features]]:
262 // detect and inform, never silently produce garbage.
263 if one_minus_k2 < NX_MAGIC_1024 {
264 // Snapshot current a into a_out for orders 1..i (already done
265 // each iteration via a_new -> a_cur copy below), set e_out,
266 // skip remaining iterations.
267 // Copy a_new into a_cur for the final loop's copy-out.
268 var sp: i64 = 0
269 while sp < p_order * 8 { a_cur[sp] = a_new[sp]; sp = sp + 1 }
270 // Force loop exit by setting i past p_order
271 i = p_order + 1
272 } else {
273 e_cur = _q30_mul(e_cur, one_minus_k2)
274 if e_cur < 1 { e_cur = 1 } // numerical safety
275 }
276
277 // Swap a_cur and a_new (copy a_new -> a_cur)
278 var cp: i64 = 0
279 while cp < p_order * 8 {
280 a_cur[cp] = a_new[cp]
281 cp = cp + 1
282 }
283 i = i + 1
284 }
285
286 // Store a_cur to a_out (p entries, p*8 bytes).
287 var co: i64 = 0
288 while co < p_order * 8 {
289 a_out[co] = a_cur[co]
290 co = co + 1
291 }
292 // Store e_out = e_cur (i64 LE).
293 e_out[0] = e_cur & 0xff
294 e_out[1] = (e_cur >> 8) & 0xff
295 e_out[2] = (e_cur >> 16) & 0xff
296 e_out[3] = (e_cur >> 24) & 0xff
297 e_out[4] = (e_cur >> 32) & 0xff
298 e_out[5] = (e_cur >> 40) & 0xff
299 e_out[6] = (e_cur >> 48) & 0xff
300 e_out[7] = (e_cur >> 56) & 0xff
301 return 0
302}