@emdzej/itw-decoder 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +118 -0
- package/README.md +127 -0
- package/dist/decode0300.js +1380 -0
- package/dist/decode0400.js +323 -0
- package/dist/index.js +56 -0
- package/dist/itw.js +59 -0
- package/dist/png.js +35 -0
- package/package.json +30 -0
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Michał Jaskólski
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the PolyForm Noncommercial License 1.0.0
|
|
6
|
+
* found in the LICENSE file in the root directory of this repository.
|
|
7
|
+
* https://polyformproject.org/licenses/noncommercial/1.0.0
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.decode0300 = decode0300;
|
|
11
|
+
const zlib_1 = require("zlib");
|
|
12
|
+
const itw_1 = require("./itw");
|
|
13
|
+
// ─── Global constants (from Ghidra data section) ────────────────────────────
|
|
14
|
+
const DAT_004ed190 = 0.5; // double: center = (range + min) * 0.5
|
|
15
|
+
const DAT_004ed198 = 1.0 / 127; // double: LL band scale (0x3f80204081020408 = 1/127)
|
|
16
|
+
const DAT_004ed1a0 = 127.0; // double: LL band offset (subtract from byte)
|
|
17
|
+
const DAT_004ed1f0 = 32.0; // float: Q15-ish divisor (0x42000000 = 32.0)
|
|
18
|
+
const DAT_004ed1d0 = 16.0; // double: level_scale_factor base
|
|
19
|
+
const DAT_004ed1d8 = 1.0 / 16; // double: level_scale_factor multiplier
|
|
20
|
+
const DAT_004ed130 = 0.125; // float: bits-to-bytes (1/8)
|
|
21
|
+
const DAT_004ed118 = 0x80; // int: extra bits flag mask
|
|
22
|
+
const DAT_004ed11c = 5; // int: buffer size multiplier
|
|
23
|
+
// ─── Cursor: tracks read position into the payload ─────────────────────────
|
|
24
|
+
class Cursor {
|
|
25
|
+
constructor(buf, offset) {
|
|
26
|
+
this.buf = buf;
|
|
27
|
+
this.pos = offset;
|
|
28
|
+
}
|
|
29
|
+
readByte() {
|
|
30
|
+
if (this.pos >= this.buf.length)
|
|
31
|
+
throw new itw_1.ITWError("cursor overrun");
|
|
32
|
+
return this.buf[this.pos++];
|
|
33
|
+
}
|
|
34
|
+
readBE16() {
|
|
35
|
+
const v = (0, itw_1.readBE16)(this.buf, this.pos);
|
|
36
|
+
this.pos += 2;
|
|
37
|
+
return v;
|
|
38
|
+
}
|
|
39
|
+
readBE32() {
|
|
40
|
+
const v = (0, itw_1.readBE32From2BE16)(this.buf, this.pos);
|
|
41
|
+
this.pos += 4;
|
|
42
|
+
return v;
|
|
43
|
+
}
|
|
44
|
+
/** Return a subarray from current position onward */
|
|
45
|
+
remaining() { return this.buf.subarray(this.pos); }
|
|
46
|
+
/** zlib inflate: reads BE16 compressed length, then inflates into destSize buffer (zero-padded) */
|
|
47
|
+
copyStreamData(destSize) {
|
|
48
|
+
const compLen = this.readBE16();
|
|
49
|
+
const compressed = this.buf.subarray(this.pos, this.pos + compLen);
|
|
50
|
+
this.pos += compLen;
|
|
51
|
+
const inflated = (0, zlib_1.inflateSync)(compressed, { maxOutputLength: destSize });
|
|
52
|
+
// Zero-pad to destSize (matching C's calloc behavior)
|
|
53
|
+
const result = new Uint8Array(destSize);
|
|
54
|
+
result.set(new Uint8Array(inflated.buffer, inflated.byteOffset, inflated.byteLength));
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ─── Bitstream reader (LSB-first, matching Ghidra's read_bits @ 004bc220) ───
|
|
59
|
+
// The C code reads bits from the LOW bit of each byte first, and accumulates
|
|
60
|
+
// the multi-bit result with the first-read bit as bit 0 (LSB).
|
|
61
|
+
class Bitstream {
|
|
62
|
+
constructor(data) {
|
|
63
|
+
this.byteIdx = 0;
|
|
64
|
+
this.bitIdx = 0; // 0..7 within current byte
|
|
65
|
+
this.curByte = 0;
|
|
66
|
+
this.data = data;
|
|
67
|
+
// Match C: first byte is loaded on first read_bit call (when bitIdx==0)
|
|
68
|
+
}
|
|
69
|
+
/** Read a single bit (LSB-first from each byte) */
|
|
70
|
+
readBit() {
|
|
71
|
+
if (this.bitIdx === 0) {
|
|
72
|
+
this.curByte = this.byteIdx < this.data.length ? this.data[this.byteIdx] : 0;
|
|
73
|
+
}
|
|
74
|
+
const bit = this.curByte & 1;
|
|
75
|
+
this.curByte >>= 1;
|
|
76
|
+
this.bitIdx++;
|
|
77
|
+
if (this.bitIdx === 8) {
|
|
78
|
+
this.bitIdx = 0;
|
|
79
|
+
this.byteIdx++;
|
|
80
|
+
}
|
|
81
|
+
return bit;
|
|
82
|
+
}
|
|
83
|
+
/** Read n bits, LSB-first: first bit read → bit 0 of result */
|
|
84
|
+
readBits(n) {
|
|
85
|
+
let val = 0;
|
|
86
|
+
let mask = 1;
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
if (this.readBit()) {
|
|
89
|
+
val |= mask;
|
|
90
|
+
}
|
|
91
|
+
mask <<= 1;
|
|
92
|
+
}
|
|
93
|
+
return val;
|
|
94
|
+
}
|
|
95
|
+
/** Return current byte-aligned position (for cursor tracking).
|
|
96
|
+
* Matches C's bitstream_finish: if bitIdx==0 return byteIdx, else byteIdx+1 */
|
|
97
|
+
get bytePos() {
|
|
98
|
+
return this.bitIdx === 0 ? this.byteIdx : this.byteIdx + 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ─── Q15 ─────────────────────────────────────────────────────────────────
|
|
102
|
+
function q15ToFloat(v) {
|
|
103
|
+
// v is a signed 16-bit int
|
|
104
|
+
const s = (v << 16) >> 16; // sign extend
|
|
105
|
+
return s / DAT_004ed1f0;
|
|
106
|
+
}
|
|
107
|
+
// ─── Wavelet filter coefficients ────────────────────────────────────────────
|
|
108
|
+
// Convert IEEE754 hex to float
|
|
109
|
+
function hexToFloat(h) {
|
|
110
|
+
const buf = new ArrayBuffer(4);
|
|
111
|
+
new DataView(buf).setInt32(0, h);
|
|
112
|
+
return new DataView(buf).getFloat32(0);
|
|
113
|
+
}
|
|
114
|
+
// Filter type 0: CDF 9/7 biorthogonal
|
|
115
|
+
const ANALYSIS_LOW_97 = [
|
|
116
|
+
0x3d5889c7, 0xbd08e1cf, 0xbdbe9b19, 0x3ec6212d,
|
|
117
|
+
// center is implicit (the filter is symmetric, stored as half-filter)
|
|
118
|
+
// Actually, from Ghidra the 8 taps plus implicit center:
|
|
119
|
+
// local_24..local_4 = 8 coefficients, center is param_2[3] (center offset)
|
|
120
|
+
].map(hexToFloat);
|
|
121
|
+
// Actually, from Ghidra wavelet_init_filters, for filter type 0:
|
|
122
|
+
// Analysis lowpass (set on param_2): local_24 array (8 elements, 9-tap symmetric)
|
|
123
|
+
const FILTER0_ANALYSIS_LOW = [
|
|
124
|
+
hexToFloat(0x3d5889c7), // 0.052861...
|
|
125
|
+
hexToFloat(0xbd08e1cf), // -0.033477...
|
|
126
|
+
hexToFloat(0xbdbe9b19), // -0.093057...
|
|
127
|
+
hexToFloat(0x3ec6212d), // 0.386942...
|
|
128
|
+
// center coefficient is implicit at index=4, but the filter has 9 taps
|
|
129
|
+
// mirror is: [idx=8]=local_4, [7]=local_8, ... [1]=local_20, [0]=local_24
|
|
130
|
+
// local_24=0x3d5889c7, local_20=0xbd08e1cf, local_1c=0xbdbe9b19, local_18=0x3ec6212d
|
|
131
|
+
// [center], local_10=0x3ec6212d, local_c=0xbdbe9b19, local_8=0xbd08e1cf, local_4=0x3d5889c7
|
|
132
|
+
];
|
|
133
|
+
// The filter_set_coeffs copies the local array into the filter's coeff array.
|
|
134
|
+
// For a 9-tap filter with center at index 4:
|
|
135
|
+
const F0_AL = [
|
|
136
|
+
hexToFloat(0x3d5889c7),
|
|
137
|
+
hexToFloat(0xbd08e1cf),
|
|
138
|
+
hexToFloat(0xbdbe9b19),
|
|
139
|
+
hexToFloat(0x3ec6212d),
|
|
140
|
+
NaN, // center — needs to be computed as (1 - 2*(sum_of_above))
|
|
141
|
+
hexToFloat(0x3ec6212d),
|
|
142
|
+
hexToFloat(0xbdbe9b19),
|
|
143
|
+
hexToFloat(0xbd08e1cf),
|
|
144
|
+
hexToFloat(0x3d5889c7),
|
|
145
|
+
];
|
|
146
|
+
// Actually, rethinking: The filter has exactly 8 coefficients stored in local_24..local_4,
|
|
147
|
+
// The filter length is 9 (for low) and 7 (for high) based on wavelet_read_filter_type.
|
|
148
|
+
// But 8 values were stored. The 9th is the center which must be computed.
|
|
149
|
+
// Wait - let's look again. local_24 through local_4 = 8 dwords = 8 coefficients.
|
|
150
|
+
// But the filter length from wavelet_read_filter_type for type 0 is: low=9, high=7.
|
|
151
|
+
// So 8 stored + center (computed from normalization) = 9 total.
|
|
152
|
+
// Actually, looking more carefully: these 8 values are the full 8-tap non-center
|
|
153
|
+
// coefficients of the 9-tap filter. The center is derived.
|
|
154
|
+
// For CDF 9/7: known coefficients...
|
|
155
|
+
// Let me just use the known CDF 9/7 values instead:
|
|
156
|
+
// CDF 9/7 analysis lowpass (9 taps):
|
|
157
|
+
// 0.026749, -0.016864, -0.078223, 0.266864, 0.602949, 0.266864, -0.078223, -0.016864, 0.026749
|
|
158
|
+
// But wait, the hex values give us slightly different numbers. Let me just use the hex-derived values.
|
|
159
|
+
// Recomputing from hex:
|
|
160
|
+
// 0x3d5889c7 = 0.05286135
|
|
161
|
+
// 0xbd08e1cf = -0.03347732
|
|
162
|
+
// 0xbdbe9b19 = -0.09305732
|
|
163
|
+
// 0x3ec6212d = 0.38694268
|
|
164
|
+
// These are the 4 non-center coefficients on one side (symmetric).
|
|
165
|
+
// The center must make the sum = 1 (for lowpass):
|
|
166
|
+
// sum_sides = 2 * (0.05286135 - 0.03347732 - 0.09305732 + 0.38694268) = 2 * 0.31326939 = 0.62653878
|
|
167
|
+
// center = 1 - 0.62653878 = 0.37346122
|
|
168
|
+
// Hmm, but this doesn't match standard CDF 9/7. Let me just trust the data.
|
|
169
|
+
// Actually, looking at the filter structure more carefully:
|
|
170
|
+
// filter_alloc(NULL, length, parity) where parity affects the center index.
|
|
171
|
+
// For type 0: AL length=9 parity=0, AH length=7 parity=0, SL length=7 parity=-1, SH length=9 parity=1
|
|
172
|
+
// The filter's center index = floor(length/2) + parity_offset
|
|
173
|
+
// For length 9, parity 0: center = 4 (0-indexed)
|
|
174
|
+
// filter_set_coeffs copies from the local array into filter[4..4+center] etc.
|
|
175
|
+
// Actually I realize: the 8 stored values in local_24..local_4 are ALL 8 non-center taps.
|
|
176
|
+
// The center coefficient is stored at local_14 = 0x3f499a81.
|
|
177
|
+
// Wait no, local_14 is used for filter type 1 (the 7/5 case).
|
|
178
|
+
// Let me re-read the decompiled code more carefully. filter_set_coeffs takes the address
|
|
179
|
+
// of local_24 (for type 0 low) or local_5c (for type 0 high).
|
|
180
|
+
// For the lowpass, local_24..local_4 = 8 values.
|
|
181
|
+
// But length = 9. So there must be a 9th value somewhere.
|
|
182
|
+
// Looking at the local variables: local_14 = 0x3f499a81
|
|
183
|
+
// But local_14 is between the filter arrays... Actually local_24 through local_4
|
|
184
|
+
// are at offsets 0x24, 0x20, 0x1c, 0x18, 0x14, 0x10, 0x0c, 0x08, 0x04
|
|
185
|
+
// Wait - that's 9 values! local_24 = index0, local_20 = index1, ..., local_04 = index8
|
|
186
|
+
// But in the decompilation only 8 are assigned:
|
|
187
|
+
// local_24, local_20, local_1c, local_18, (missing local_14), local_10, local_c, local_8, local_4
|
|
188
|
+
// local_14 = 0x3f499a81 is assigned! It's the center coefficient!
|
|
189
|
+
// So the 9 coefficients of analysis lowpass (type 0) are:
|
|
190
|
+
// local_24=0x3d5889c7, local_20=0xbd08e1cf, local_1c=0xbdbe9b19, local_18=0x3ec6212d,
|
|
191
|
+
// local_14=0x3f499a81, local_10=0x3ec6212d, local_c=0xbdbe9b19, local_8=0xbd08e1cf, local_4=0x3d5889c7
|
|
192
|
+
// Wait, let's check: is local_14 used for type 0 or type 1?
|
|
193
|
+
// In the decompiled code:
|
|
194
|
+
// local_14 = 0x3f499a81; (assigned unconditionally at top of function)
|
|
195
|
+
// For type 0: filter_set_coeffs(&local_24, param_2) → copies starting from local_24
|
|
196
|
+
// For type 1: filter_set_coeffs(&local_40, param_2), filter_set_coeffs(&local_70, param_4)
|
|
197
|
+
//
|
|
198
|
+
// local_24 through local_4 means bytes at [ebp-0x24] through [ebp-0x4].
|
|
199
|
+
// The contiguous block from [ebp-0x24] to [ebp-0x04] is 0x24-0x04=0x20=32 bytes,
|
|
200
|
+
// but with 4-byte alignment: [ebp-0x24], [ebp-0x20], [ebp-0x1c], [ebp-0x18],
|
|
201
|
+
// [ebp-0x14], [ebp-0x10], [ebp-0x0c], [ebp-0x08], [ebp-0x04] = 9 float values!
|
|
202
|
+
// So local_14 IS the center coefficient of the analysis lowpass filter!
|
|
203
|
+
// OK but local_14 was also assigned differently in type 1 section...
|
|
204
|
+
// Looking at assignments: local_14 = 0x3f499a81 is set at the top (for type 0 center)
|
|
205
|
+
// But local_50 = 0x3f511889 is also set. Let me check what local_50 is.
|
|
206
|
+
// For analysis high (type 0): local_5c..local_44
|
|
207
|
+
// local_5c=0xbdb1a91a, local_58=0xbd609caf, local_54=0x3ee16f3a, local_50=0x3f511889
|
|
208
|
+
// local_4c=0x3ee16f3a, local_48=0xbd609caf, local_44=0xbdb1a91a
|
|
209
|
+
// That's 7 values at local_5c through local_44. Perfect for the 7-tap high filter.
|
|
210
|
+
// But wait: local_5c, local_58, local_54, local_50, local_4c, local_48, local_44 = 7 values.
|
|
211
|
+
// So: analysis lowpass = 9 taps: local_24(0x3d5889c7), local_20(0xbd08e1cf), local_1c(0xbdbe9b19),
|
|
212
|
+
// local_18(0x3ec6212d), local_14(0x3f499a81), local_10(0x3ec6212d), local_c(0xbdbe9b19),
|
|
213
|
+
// local_8(0xbd08e1cf), local_4(0x3d5889c7)
|
|
214
|
+
// Analysis highpass = 7 taps: local_5c(0xbdb1a91a), local_58(0xbd609caf), local_54(0x3ee16f3a),
|
|
215
|
+
// local_50(0x3f511889), local_4c(0x3ee16f3a), local_48(0xbd609caf), local_44(0xbdb1a91a)
|
|
216
|
+
// For filter type 1 (7/5 biorthogonal):
|
|
217
|
+
// Analysis lowpass = 7 taps: local_40..local_28
|
|
218
|
+
// local_40=0xbc2f8af9, local_3c=0xbd5b6db7, local_38=0x3e857c58, local_34=0x3f1b6db7,
|
|
219
|
+
// local_30=0x3e857c58, local_2c=0xbd5b6db7, local_28=0xbc2f8af9
|
|
220
|
+
// Analysis highpass = 5 taps: local_70..local_60
|
|
221
|
+
// local_70=0xbd4ccccd, local_6c=0x3e800000, local_68=0x3f19999a,
|
|
222
|
+
// local_64=0x3e800000, local_60=0xbd4ccccd
|
|
223
|
+
function buildFilterCoeffs(filterType) {
|
|
224
|
+
if (filterType === 0) {
|
|
225
|
+
// CDF 9/7
|
|
226
|
+
return {
|
|
227
|
+
analysisLowLen: 9,
|
|
228
|
+
analysisLowCoeffs: [
|
|
229
|
+
hexToFloat(0x3d5889c7), hexToFloat(0xbd08e1cf), hexToFloat(0xbdbe9b19),
|
|
230
|
+
hexToFloat(0x3ec6212d), hexToFloat(0x3f499a81), hexToFloat(0x3ec6212d),
|
|
231
|
+
hexToFloat(0xbdbe9b19), hexToFloat(0xbd08e1cf), hexToFloat(0x3d5889c7),
|
|
232
|
+
],
|
|
233
|
+
analysisHighLen: 7,
|
|
234
|
+
analysisHighCoeffs: [
|
|
235
|
+
hexToFloat(0xbdb1a91a), hexToFloat(0xbd609caf), hexToFloat(0x3ee16f3a),
|
|
236
|
+
hexToFloat(0x3f511889), hexToFloat(0x3ee16f3a), hexToFloat(0xbd609caf),
|
|
237
|
+
hexToFloat(0xbdb1a91a),
|
|
238
|
+
],
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
else if (filterType === 1) {
|
|
242
|
+
// 7/5 biorthogonal, scaled by sqrt(2)
|
|
243
|
+
const s = Math.sqrt(2.0);
|
|
244
|
+
return {
|
|
245
|
+
analysisLowLen: 7,
|
|
246
|
+
analysisLowCoeffs: [
|
|
247
|
+
hexToFloat(0xbc2f8af9) * s, hexToFloat(0xbd5b6db7) * s, hexToFloat(0x3e857c58) * s,
|
|
248
|
+
hexToFloat(0x3f1b6db7) * s, hexToFloat(0x3e857c58) * s, hexToFloat(0xbd5b6db7) * s,
|
|
249
|
+
hexToFloat(0xbc2f8af9) * s,
|
|
250
|
+
],
|
|
251
|
+
analysisHighLen: 5,
|
|
252
|
+
analysisHighCoeffs: [
|
|
253
|
+
hexToFloat(0xbd4ccccd) * s, hexToFloat(0x3e800000) * s, hexToFloat(0x3f19999a) * s,
|
|
254
|
+
hexToFloat(0x3e800000) * s, hexToFloat(0xbd4ccccd) * s,
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
throw new itw_1.ITWError(`unsupported filter type ${filterType}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Derive mirror filter (for synthesis from analysis):
|
|
263
|
+
// filter_derive_mirror(center, src, dest):
|
|
264
|
+
// First loop: from center down to 0, sign starts at +1 and alternates
|
|
265
|
+
// Second loop: from center+1 up to len-1, sign restarts at +1 and alternates
|
|
266
|
+
// dest[i] = src[i] * sign (NO reversal — same index for src and dest)
|
|
267
|
+
//
|
|
268
|
+
// Sign pattern for 9-tap (center=4): [+1,-1,+1,-1,+1, +1,-1,+1,-1]
|
|
269
|
+
// Sign pattern for 7-tap (center=3): [-1,+1,-1,+1, +1,-1,+1]
|
|
270
|
+
function deriveMirror(coeffs, center) {
|
|
271
|
+
const len = coeffs.length;
|
|
272
|
+
const result = new Array(len);
|
|
273
|
+
// First loop: center down to 0, sign alternates starting at +1
|
|
274
|
+
let sign = 1;
|
|
275
|
+
for (let i = center; i >= 0; i--) {
|
|
276
|
+
result[i] = coeffs[i] * sign;
|
|
277
|
+
sign = -sign;
|
|
278
|
+
}
|
|
279
|
+
// Second loop: center+1 up to len-1, sign starts at -1
|
|
280
|
+
// (Ghidra's loop restarts at center with +1, flips to -1, then writes center+1 with -1)
|
|
281
|
+
sign = -1;
|
|
282
|
+
for (let i = center + 1; i < len; i++) {
|
|
283
|
+
result[i] = coeffs[i] * sign;
|
|
284
|
+
sign = -sign;
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
288
|
+
function makeFilter(coeffs, length, parity) {
|
|
289
|
+
return { coeffs, length, center: Math.floor(length / 2), parity };
|
|
290
|
+
}
|
|
291
|
+
function initFilters(filterType) {
|
|
292
|
+
const { analysisLowCoeffs, analysisHighCoeffs, analysisLowLen, analysisHighLen } = buildFilterCoeffs(filterType);
|
|
293
|
+
// From Ghidra wavelet_init_filters:
|
|
294
|
+
// param_2 = analysis low (length = analysisLowLen, parity 0)
|
|
295
|
+
// param_4 = analysis high (length = analysisHighLen, parity 0)
|
|
296
|
+
// param_3 = synthesis low (length = analysisHighLen, parity -1) — derived from analysis high
|
|
297
|
+
// param_5 = synthesis high (length = analysisLowLen, parity 1) — derived from analysis low
|
|
298
|
+
// filter_derive_mirror(center, src, dest):
|
|
299
|
+
const analysisLowCenter = Math.floor(analysisLowLen / 2);
|
|
300
|
+
const analysisHighCenter = Math.floor(analysisHighLen / 2);
|
|
301
|
+
const synthHighCoeffs = deriveMirror(analysisLowCoeffs, analysisLowCenter);
|
|
302
|
+
const synthLowCoeffs = deriveMirror(analysisHighCoeffs, analysisHighCenter);
|
|
303
|
+
// Reconstruction uses: puVar6 (analysis high, parity 0) and puVar7 (synthesis high, parity 1)
|
|
304
|
+
// wavelet_reconstruct_all(puVar6, puVar7, pyramid, image)
|
|
305
|
+
// where puVar6 = filter_alloc(NULL, analysisHighLen, 0) → analysis high
|
|
306
|
+
// puVar7 = filter_alloc(NULL, analysisLowLen, 1) → synthesis high
|
|
307
|
+
const analysisHigh = makeFilter(analysisHighCoeffs, analysisHighLen, 0);
|
|
308
|
+
const synthHigh = makeFilter(synthHighCoeffs, analysisLowLen, 1);
|
|
309
|
+
const synthLow = makeFilter(synthLowCoeffs, analysisHighLen, -1);
|
|
310
|
+
// EXPERIMENT: try synthLow (g0) instead of analysisHigh (h1) as filter1
|
|
311
|
+
// Standard biorthogonal reconstruction uses g0 and g1 (both synthesis filters)
|
|
312
|
+
// TIS.exe appears to use h1 and g1 — but maybe the polyphase structure compensates?
|
|
313
|
+
return { reconstructFilter1: analysisHigh, reconstructFilter2: synthHigh };
|
|
314
|
+
// Alternative: return { reconstructFilter1: synthLow, reconstructFilter2: synthHigh };
|
|
315
|
+
}
|
|
316
|
+
// ─── Fischer rank coding tables ─────────────────────────────────────────────
|
|
317
|
+
// Three tables are used:
|
|
318
|
+
// 1. BASE TABLE: cumT(q, m) = number of signed q-tuples with sum(|xi|) <= m
|
|
319
|
+
// Computed mathematically. Used to build the diff table.
|
|
320
|
+
// 2. RANK TABLE: Hardcoded bit lengths from the binary. Used to determine
|
|
321
|
+
// how many bits to read for each codeword from the bitstream.
|
|
322
|
+
// 3. DIFF TABLE: exact count T(q, m) = base[q][m] - base[q][m-1].
|
|
323
|
+
// Passed to fischerDecode for combinatorial unranking.
|
|
324
|
+
const MAX_Q = 9; // rows 0..8
|
|
325
|
+
const MAX_M_LARGE = 201; // columns for rows 0-4
|
|
326
|
+
const MAX_M_SMALL = 31; // columns for rows 5-8
|
|
327
|
+
function binomial(n, k) {
|
|
328
|
+
if (k < 0 || k > n)
|
|
329
|
+
return 0;
|
|
330
|
+
if (k === 0 || k === n)
|
|
331
|
+
return 1;
|
|
332
|
+
let result = 1;
|
|
333
|
+
for (let i = 0; i < Math.min(k, n - k); i++) {
|
|
334
|
+
result = result * (n - i) / (i + 1);
|
|
335
|
+
}
|
|
336
|
+
return Math.round(result);
|
|
337
|
+
}
|
|
338
|
+
/** Number of signed integer q-tuples with sum of absolute values exactly m */
|
|
339
|
+
function countExact(q, m) {
|
|
340
|
+
if (m === 0)
|
|
341
|
+
return 1;
|
|
342
|
+
let sum = 0;
|
|
343
|
+
for (let j = 1; j <= Math.min(q, m); j++) {
|
|
344
|
+
sum += binomial(q, j) * binomial(m - 1, j - 1) * (1 << j);
|
|
345
|
+
}
|
|
346
|
+
return sum;
|
|
347
|
+
}
|
|
348
|
+
/** Cumulative count: number of signed integer q-tuples with sum of abs values <= m */
|
|
349
|
+
function countCumulative(q, m) {
|
|
350
|
+
let total = 0;
|
|
351
|
+
for (let k = 0; k <= m; k++) {
|
|
352
|
+
total += countExact(q, k);
|
|
353
|
+
}
|
|
354
|
+
return total;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Build the base table (9 × maxM).
|
|
358
|
+
* base_table[q][m] = cumT(q, m) = cumulative count of vectors.
|
|
359
|
+
*/
|
|
360
|
+
function buildBaseTable() {
|
|
361
|
+
const table = [];
|
|
362
|
+
for (let q = 0; q < MAX_Q; q++) {
|
|
363
|
+
const maxM = q < 5 ? MAX_M_LARGE : MAX_M_SMALL;
|
|
364
|
+
const row = new Array(maxM).fill(0);
|
|
365
|
+
row[0] = 1;
|
|
366
|
+
if (q === 0) {
|
|
367
|
+
for (let m = 0; m < maxM; m++)
|
|
368
|
+
row[m] = 1;
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
for (let m = 1; m < maxM; m++) {
|
|
372
|
+
row[m] = countCumulative(q, m);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
table.push(row);
|
|
376
|
+
}
|
|
377
|
+
return table;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Build the diff table from the base table.
|
|
381
|
+
* diff[q][m] = base[q][m] - base[q][m-1] = T(q, m) = exact count for magnitude m.
|
|
382
|
+
* diff[q][0] = 1 for all q; diff[0][m>=1] = 0.
|
|
383
|
+
* This table is used by fischerDecode for combinatorial unranking.
|
|
384
|
+
*/
|
|
385
|
+
function buildDiffTable(baseTable) {
|
|
386
|
+
const table = [];
|
|
387
|
+
for (let q = 0; q < MAX_Q; q++) {
|
|
388
|
+
const maxM = q < 5 ? MAX_M_LARGE : MAX_M_SMALL;
|
|
389
|
+
const row = new Array(maxM).fill(0);
|
|
390
|
+
row[0] = 1; // T(q, 0) = 1
|
|
391
|
+
if (q === 0) {
|
|
392
|
+
// Row 0: diff[0][0] = 1, diff[0][m>=1] = 0
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
for (let m = 1; m < maxM; m++) {
|
|
396
|
+
row[m] = baseTable[q][m] - baseTable[q][m - 1];
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
table.push(row);
|
|
400
|
+
}
|
|
401
|
+
return table;
|
|
402
|
+
}
|
|
403
|
+
// ─── Hardcoded rank table (bit lengths for codewords) ───────────────────────
|
|
404
|
+
// From Ghidra fischer_build_rank_table: 603 hardcoded values.
|
|
405
|
+
// Indexed as: rankTable[quant][magnitude] → number of bits for codeword.
|
|
406
|
+
// Only quant=2 (201 entries), quant=4 (201 entries), quant=8 (31 entries) are stored.
|
|
407
|
+
// prettier-ignore
|
|
408
|
+
const RANK_TABLE_Q2 = [
|
|
409
|
+
0, 2, 3, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7,
|
|
410
|
+
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8,
|
|
411
|
+
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
|
|
412
|
+
8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
|
|
413
|
+
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
|
|
414
|
+
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
|
|
415
|
+
9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
|
416
|
+
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
|
417
|
+
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
|
418
|
+
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
|
|
419
|
+
0,
|
|
420
|
+
];
|
|
421
|
+
// prettier-ignore
|
|
422
|
+
const RANK_TABLE_Q4 = [
|
|
423
|
+
0, 3, 5, 7, 8, 9, 10, 10, 11, 11, 12, 12, 13, 13, 13, 14, 14, 14, 14, 15,
|
|
424
|
+
15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18,
|
|
425
|
+
18, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 20,
|
|
426
|
+
20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21,
|
|
427
|
+
21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22,
|
|
428
|
+
22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23,
|
|
429
|
+
23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23,
|
|
430
|
+
23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
|
431
|
+
24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
|
|
432
|
+
24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
|
|
433
|
+
0,
|
|
434
|
+
];
|
|
435
|
+
// prettier-ignore
|
|
436
|
+
const RANK_TABLE_Q8 = [
|
|
437
|
+
0, 4, 7, 10, 12, 14, 15, 17, 18, 19, 20, 21, 22, 22, 23, 24,
|
|
438
|
+
24, 25, 26, 26, 27, 27, 27, 28, 28, 29, 29, 30, 30, 30, 0,
|
|
439
|
+
];
|
|
440
|
+
/**
|
|
441
|
+
* Build the rank table structure from the hardcoded arrays.
|
|
442
|
+
* Maps quant → magnitude → bit length.
|
|
443
|
+
* For quant < 2 (quant=1): bit length is computed as ceil(log2(m*2+1)).
|
|
444
|
+
* For quant not in {2,4,8}: returns 0 (shouldn't occur in practice).
|
|
445
|
+
*/
|
|
446
|
+
function buildRankTable() {
|
|
447
|
+
// We need rows for quant=0..8 to match the int_array(9, 0xC9, 1) structure.
|
|
448
|
+
// The Ghidra code stores into: row=2 (quant=2), row=4 (quant=4), row=8 (quant=8).
|
|
449
|
+
// All other rows are left at 0 (the table is zero-initialized by int_array_alloc).
|
|
450
|
+
const table = [];
|
|
451
|
+
for (let q = 0; q < MAX_Q; q++) {
|
|
452
|
+
const maxM = q < 5 ? MAX_M_LARGE : MAX_M_SMALL;
|
|
453
|
+
const row = new Array(maxM).fill(0);
|
|
454
|
+
if (q === 2) {
|
|
455
|
+
for (let m = 0; m < maxM; m++)
|
|
456
|
+
row[m] = RANK_TABLE_Q2[m];
|
|
457
|
+
}
|
|
458
|
+
else if (q === 4) {
|
|
459
|
+
for (let m = 0; m < maxM; m++)
|
|
460
|
+
row[m] = RANK_TABLE_Q4[m];
|
|
461
|
+
}
|
|
462
|
+
else if (q === 8) {
|
|
463
|
+
for (let m = 0; m < Math.min(maxM, RANK_TABLE_Q8.length); m++)
|
|
464
|
+
row[m] = RANK_TABLE_Q8[m];
|
|
465
|
+
}
|
|
466
|
+
table.push(row);
|
|
467
|
+
}
|
|
468
|
+
return table;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Look up a value from a 2D table. Returns the value at table[q][m],
|
|
472
|
+
* or 0 if out of bounds (matches C zero-initialized array behavior).
|
|
473
|
+
*/
|
|
474
|
+
function tableLookup(table, q, m) {
|
|
475
|
+
if (q < 0 || q >= table.length)
|
|
476
|
+
return 0;
|
|
477
|
+
const row = table[q];
|
|
478
|
+
if (m < 0 || m >= row.length)
|
|
479
|
+
return 0;
|
|
480
|
+
return row[m];
|
|
481
|
+
}
|
|
482
|
+
// ─── Fischer decode ──────────────────────────────────────────────────────────
|
|
483
|
+
// Combinatorial unranking: given a codeword (rank), magnitude sum, and output length,
|
|
484
|
+
// decode the unique signed integer tuple.
|
|
485
|
+
// Uses the DIFF TABLE (exact counts T(q,m)) for lookups.
|
|
486
|
+
// T(q, m) = number of signed q-tuples with sum(|xi|) EXACTLY equal to m.
|
|
487
|
+
// The unranking partitions by first-position value:
|
|
488
|
+
// T(n-1, m) tuples where pos[0]=0 (same total magnitude distributed among n-1 remaining positions)
|
|
489
|
+
// T(n-1, m-a) tuples where |pos[0]|=a (for each sign), counted as 2*T(n-1, m-a)
|
|
490
|
+
// From Ghidra fischer_decode:
|
|
491
|
+
// - uVar3 = outLen (*out_table)
|
|
492
|
+
// - uVar5 starts at outLen, decremented each iteration
|
|
493
|
+
// - calc_rank_bit_length(rank_table, uVar5-1, local_10, 0) looks up diffTable[remaining_positions-1][remaining_magnitude]
|
|
494
|
+
// - local_10 = remaining magnitude, local_c = output index
|
|
495
|
+
function fischerDecode(outLen, codeword, magnitudeSum, diffTable) {
|
|
496
|
+
const out = new Array(outLen).fill(0);
|
|
497
|
+
if (magnitudeSum === 0)
|
|
498
|
+
return out;
|
|
499
|
+
let remaining = magnitudeSum; // local_10
|
|
500
|
+
let runningTotal = 0; // iVar4
|
|
501
|
+
let outIdx = 0; // local_c
|
|
502
|
+
let remainingPositions = outLen; // uVar5
|
|
503
|
+
while (outIdx < outLen) {
|
|
504
|
+
if (codeword === runningTotal) {
|
|
505
|
+
out[outIdx] = 0;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
// Count of tuples with 0 at this position: T(n-1, m) = exact count of
|
|
509
|
+
// (n-1)-tuples with sum of abs values exactly equal to remaining magnitude
|
|
510
|
+
const zeroCount = tableLookup(diffTable, remainingPositions - 1, remaining);
|
|
511
|
+
if (codeword < zeroCount + runningTotal) {
|
|
512
|
+
// Zero at this position
|
|
513
|
+
out[outIdx] = 0;
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
// Non-zero: find the absolute value
|
|
517
|
+
let iVar4 = runningTotal + zeroCount;
|
|
518
|
+
let absVal = 1; // local_8
|
|
519
|
+
while (true) {
|
|
520
|
+
const subCount = tableLookup(diffTable, remainingPositions - 1, remaining - absVal);
|
|
521
|
+
if (codeword < iVar4 + subCount * 2)
|
|
522
|
+
break;
|
|
523
|
+
iVar4 += subCount * 2;
|
|
524
|
+
absVal++;
|
|
525
|
+
if (absVal > remaining + 1) {
|
|
526
|
+
console.error(`fischerDecode INFINITE LOOP: outLen=${outLen} cw=${codeword} magSum=${magnitudeSum} remaining=${remaining} absVal=${absVal} iVar4=${iVar4} outIdx=${outIdx}`);
|
|
527
|
+
throw new Error('fischerDecode infinite loop');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Determine sign: positive or negative
|
|
531
|
+
const subCount = tableLookup(diffTable, remainingPositions - 1, remaining - absVal);
|
|
532
|
+
if (codeword >= iVar4 && codeword < subCount + iVar4) {
|
|
533
|
+
// Positive
|
|
534
|
+
out[outIdx] = absVal;
|
|
535
|
+
}
|
|
536
|
+
// Check if negative
|
|
537
|
+
if (subCount + iVar4 <= codeword) {
|
|
538
|
+
out[outIdx] = -absVal;
|
|
539
|
+
runningTotal = iVar4 + subCount;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
runningTotal = iVar4;
|
|
543
|
+
}
|
|
544
|
+
remaining -= absVal;
|
|
545
|
+
}
|
|
546
|
+
remainingPositions--;
|
|
547
|
+
outIdx++;
|
|
548
|
+
}
|
|
549
|
+
// End fixup: if remaining magnitude > 0, adjust the last position
|
|
550
|
+
// From Ghidra: if (0 < local_10) { last = outLen-1; val = out[last]; out[last] = remaining - abs(val); }
|
|
551
|
+
// Wait, the Ghidra code says:
|
|
552
|
+
// iVar4 = uVar3 - 1 (= outLen - 1)
|
|
553
|
+
// uVar3 = int_table_get(out_table, local_c) [local_c = outIdx at loop end]
|
|
554
|
+
// int_table_set(out_table, local_10 - abs(uVar3), iVar4)
|
|
555
|
+
// So: out[outLen-1] = remaining - abs(out[outIdx])
|
|
556
|
+
// Hmm, that means it reads from outIdx (which is the last written position + 1 or loop-end)
|
|
557
|
+
// and writes to outLen-1. Let me re-read...
|
|
558
|
+
// After the loop: local_c was incremented past the last written position.
|
|
559
|
+
// "uVar3 = int_table_get(out_table, local_c)" reads the NEXT position (which might be 0).
|
|
560
|
+
// Actually no: the loop does: outIdx++ at end of each iteration, so after the break,
|
|
561
|
+
// outIdx points to the position where we wrote 0 and broke. If we didn't break,
|
|
562
|
+
// outIdx = outLen after the loop.
|
|
563
|
+
// Hmm wait, looking at Ghidra more carefully:
|
|
564
|
+
// The "if (codeword == iVar4) { set 0 at local_c; break; }" breaks BEFORE incrementing.
|
|
565
|
+
// In the normal path, the loop body ends with: uVar5--; uVar2=get(out,local_c); local_c++; ...
|
|
566
|
+
// So when breaking on codeword==runningTotal, local_c is the position where 0 was set.
|
|
567
|
+
//
|
|
568
|
+
// The end fixup: "uVar3 = int_table_get(out_table, local_c)" — local_c is the last-written index
|
|
569
|
+
// (from the break) or the current outIdx. Then out[outLen-1] = remaining - abs(out[local_c]).
|
|
570
|
+
//
|
|
571
|
+
// Hmm, that's odd. Let me re-read the Ghidra code more carefully...
|
|
572
|
+
// Actually in the non-break path: the last line of the loop is:
|
|
573
|
+
// uVar2 = int_table_get(out_table, local_c); local_c++; local_10 -= abs(uVar2);
|
|
574
|
+
// So local_c is incremented AFTER reading. At loop end, local_c = outLen.
|
|
575
|
+
// But the fixup reads from local_c which is outLen — that's out of bounds!
|
|
576
|
+
// Unless... the fixup only triggers when remaining > 0 AND the break happened.
|
|
577
|
+
// In the break case, local_c is the break position (not incremented).
|
|
578
|
+
// Actually wait, looking at the Ghidra code structure:
|
|
579
|
+
// do {
|
|
580
|
+
// if (codeword == iVar4) { set(0, local_c); break; }
|
|
581
|
+
// ...process...
|
|
582
|
+
// uVar5--;
|
|
583
|
+
// uVar2 = get(out, local_c);
|
|
584
|
+
// local_c++;
|
|
585
|
+
// local_10 -= abs(uVar2);
|
|
586
|
+
// } while (local_c < outLen);
|
|
587
|
+
//
|
|
588
|
+
// if (remaining > 0) {
|
|
589
|
+
// iVar4 = outLen - 1;
|
|
590
|
+
// uVar3 = get(out, local_c);
|
|
591
|
+
// set(out, remaining - abs(uVar3), iVar4);
|
|
592
|
+
// }
|
|
593
|
+
//
|
|
594
|
+
// When the break fires: local_c = break position. remaining was not updated.
|
|
595
|
+
// The fixup then reads out[break_position] (which was just set to 0) → abs = 0
|
|
596
|
+
// Then sets out[outLen-1] = remaining - 0 = remaining.
|
|
597
|
+
// This makes sense! It dumps the leftover magnitude into the last position.
|
|
598
|
+
if (remaining > 0) {
|
|
599
|
+
const lastIdx = outLen - 1;
|
|
600
|
+
// C code reads out[outIdx] which may be out of bounds (outIdx == outLen after full loop).
|
|
601
|
+
// In C, the zero-initialized array has 0 beyond bounds. In JS, out[outLen] = undefined.
|
|
602
|
+
const lastVal = outIdx < outLen ? out[outIdx] : 0;
|
|
603
|
+
const absLast = Math.abs(lastVal);
|
|
604
|
+
out[lastIdx] = remaining - absLast;
|
|
605
|
+
}
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
function matrixCreate(w, h) {
|
|
609
|
+
return { data: new Float32Array(w * h), width: w, height: h };
|
|
610
|
+
}
|
|
611
|
+
function matrixGet(m, x, y) {
|
|
612
|
+
return m.data[y * m.width + x];
|
|
613
|
+
}
|
|
614
|
+
function matrixSet(m, x, y, v) {
|
|
615
|
+
m.data[y * m.width + x] = v;
|
|
616
|
+
}
|
|
617
|
+
function splitEvenOdd(n) {
|
|
618
|
+
// From Ghidra split_even_odd: if n is even, both halves = n/2
|
|
619
|
+
// If n is odd, even = (n+1)/2, odd = (n-1)/2
|
|
620
|
+
if ((n & 1) === 0) {
|
|
621
|
+
return [n / 2, n / 2];
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
return [(n + 1) / 2, (n - 1) / 2];
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function pyramidCreate(width, height, numLevels) {
|
|
628
|
+
const levels = [];
|
|
629
|
+
let w = width, h = height;
|
|
630
|
+
for (let lev = 0; lev < numLevels; lev++) {
|
|
631
|
+
const [ew, ow] = splitEvenOdd(w);
|
|
632
|
+
const [eh, oh] = splitEvenOdd(h);
|
|
633
|
+
// Subbands: LL(ew×eh), LH(ew×oh), HL(ow×eh), HH(ow×oh)
|
|
634
|
+
const ll = matrixCreate(ew, eh);
|
|
635
|
+
const lh = matrixCreate(ew, oh);
|
|
636
|
+
const hl = matrixCreate(ow, eh);
|
|
637
|
+
const hh = matrixCreate(ow, oh);
|
|
638
|
+
levels.push({ subbands: [ll, lh, hl, hh] });
|
|
639
|
+
// Next level operates on LL
|
|
640
|
+
w = ew;
|
|
641
|
+
h = eh;
|
|
642
|
+
}
|
|
643
|
+
return { levels, numLevels };
|
|
644
|
+
}
|
|
645
|
+
// ─── Polyphase synthesis (from Ghidra polyphase_convolve @ 004bc940) ────────
|
|
646
|
+
//
|
|
647
|
+
// The synthesis filter bank reconstructs the signal from subband samples.
|
|
648
|
+
// For a filter with center index `c`, parity offset `p = -filter_parity`,
|
|
649
|
+
// the convolution for output sample i is:
|
|
650
|
+
//
|
|
651
|
+
// out[i] = Σ_{j=-c}^{c} coeffs[c - j] * src_extended[(i + c + p + j) / 2]
|
|
652
|
+
// (only summing where (i + c + p + j) is even)
|
|
653
|
+
//
|
|
654
|
+
// Edge extension: src_extended[k] = src[boundary - k] for k outside [0, srcLen)
|
|
655
|
+
// where boundary depends on the extension type (param4, param5).
|
|
656
|
+
/**
|
|
657
|
+
* Edge-extend a source sample. Matches Ghidra's edge_extend_sample.
|
|
658
|
+
* @param src source array
|
|
659
|
+
* @param idx requested index (may be negative or >= srcLen)
|
|
660
|
+
* @param srcLen number of valid samples
|
|
661
|
+
* @param boundary reflection boundary
|
|
662
|
+
*/
|
|
663
|
+
function edgeExtend(src, idx, srcLen, boundary) {
|
|
664
|
+
if (idx >= 0 && idx < srcLen)
|
|
665
|
+
return src[idx];
|
|
666
|
+
return src[boundary - idx];
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* 1D polyphase synthesis convolution.
|
|
670
|
+
* Matches Ghidra's polyphase_convolve @ 004bc940 / FUN_004bcdc0 (add variant).
|
|
671
|
+
*
|
|
672
|
+
* Three sections exactly as in the original:
|
|
673
|
+
* 1. Slow start (0 to center-parity): edge extension with leftBound
|
|
674
|
+
* 2. Fast interior: direct polyphase access (no edge extension needed)
|
|
675
|
+
* 3. Slow end (remainder to dstLen): edge extension with rightBound
|
|
676
|
+
*
|
|
677
|
+
* CRITICAL: The slow-start uses leftBound for ALL out-of-bounds samples,
|
|
678
|
+
* and slow-end uses rightBound for ALL out-of-bounds samples.
|
|
679
|
+
* This differs from our previous per-tap boundary selection (k < 0 ? left : right)
|
|
680
|
+
* which caused visual artifacts.
|
|
681
|
+
*/
|
|
682
|
+
function polyphaseConvolve1D(filter, dst, dstLen, src, srcLen, param4, param5, add // false = set (overwrite), true = add (accumulate)
|
|
683
|
+
) {
|
|
684
|
+
const c = filter.center; // filter[3] = (length-1)/2
|
|
685
|
+
const p = -filter.parity; // filter[5] = -parity from filter_alloc
|
|
686
|
+
// Edge extension boundaries (from edge_extension_setup)
|
|
687
|
+
const leftBound = (param4 === 1) ? 0 : -1;
|
|
688
|
+
const rightBound = (param5 === 1) ? srcLen * 2 - 2 : srcLen * 2 - 1;
|
|
689
|
+
// Section boundaries (from Ghidra):
|
|
690
|
+
// slowStartEnd = center + parity (= center - (-parity) = center - p... wait, iVar5 = filter[5] = -parity)
|
|
691
|
+
// In Ghidra: uVar8 = iVar4 - iVar5, where iVar4=center, iVar5=*(param_1+0x14)=filter[5]=-parity
|
|
692
|
+
// BUT iVar5 is stored as: EAX = filter[5], NEG EAX → iVar5_stored = -filter[5] = parity
|
|
693
|
+
// Wait — re-reading the ASM:
|
|
694
|
+
// MOV EAX,[EDI+0x14] → EAX = filter[5] = -parity_param
|
|
695
|
+
// NEG EAX → EAX = parity_param
|
|
696
|
+
// MOV [ESP+0x28],EAX → stored_parity = parity_param
|
|
697
|
+
// Then: uVar8 (slowStartEnd) = center + stored_parity... wait no:
|
|
698
|
+
// ADD EAX,ESI → EAX = parity_param + center (at 004bc95b)
|
|
699
|
+
// This is stored at [ESP+0x1c] = center + parity_param
|
|
700
|
+
// That's the slow-start end boundary.
|
|
701
|
+
//
|
|
702
|
+
// And fastEnd = dstLen - center - parity_param - 2
|
|
703
|
+
// (from: EAX = dstLen - center, SUB EAX, parity_param, SUB EAX, 2)
|
|
704
|
+
//
|
|
705
|
+
// With our variable names: p = -parity_param, so:
|
|
706
|
+
// slowStartEnd = center - p (= center + parity_param)
|
|
707
|
+
// fastEnd = dstLen - center + p - 2 (= dstLen - center - parity_param - 2)
|
|
708
|
+
const slowStartEnd = c - p; // = center + parity_param
|
|
709
|
+
const fastEnd = dstLen - c + p - 2; // = dstLen - center - parity_param - 2
|
|
710
|
+
// ── Section 1: Slow start (i from 0 to slowStartEnd-1) ──
|
|
711
|
+
// Uses leftBound for ALL out-of-bounds edge extension
|
|
712
|
+
for (let i = 0; i < slowStartEnd && i < dstLen; i++) {
|
|
713
|
+
let sum = 0;
|
|
714
|
+
for (let j = -c; j <= c; j++) {
|
|
715
|
+
const upIdx = i + p + j;
|
|
716
|
+
if ((upIdx & 1) === 0) {
|
|
717
|
+
const k = upIdx >> 1;
|
|
718
|
+
const sample = edgeExtend(src, k, srcLen, leftBound);
|
|
719
|
+
sum += sample * filter.coeffs[c - j];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (add) {
|
|
723
|
+
dst[i] += sum;
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
dst[i] = sum;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// ── Section 2: Fast interior (i from slowStartEnd to fastEnd-1, stepping by 2) ──
|
|
730
|
+
// Processes two outputs per iteration using polyphase decomposition.
|
|
731
|
+
// No edge extension needed — all source indices are guaranteed in-bounds.
|
|
732
|
+
//
|
|
733
|
+
// From Ghidra: pfVar10 = &coeffs[center * 2] (center of coeff array, float ptr)
|
|
734
|
+
// Even output (i): taps at coeffs[center], coeffs[center-2], coeffs[center-4], ...
|
|
735
|
+
// multiplied by src[srcIdx], src[srcIdx+1], src[srcIdx+2], ...
|
|
736
|
+
// Odd output (i+1): taps at coeffs[center-1], coeffs[center-3], coeffs[center-5], ...
|
|
737
|
+
// multiplied by src[srcIdx+1], src[srcIdx+2], src[srcIdx+3], ...
|
|
738
|
+
//
|
|
739
|
+
// The fast interior is functionally equivalent to the slow path for in-bounds samples,
|
|
740
|
+
// just optimized with pointer arithmetic. We use the slow-path formula here since
|
|
741
|
+
// it's clearer and produces identical results for in-bounds samples.
|
|
742
|
+
{
|
|
743
|
+
let i = slowStartEnd;
|
|
744
|
+
while (i < fastEnd) {
|
|
745
|
+
// Even output sample (i)
|
|
746
|
+
let sum0 = 0;
|
|
747
|
+
for (let j = -c; j <= c; j++) {
|
|
748
|
+
const upIdx = i + p + j;
|
|
749
|
+
if ((upIdx & 1) === 0) {
|
|
750
|
+
const k = upIdx >> 1;
|
|
751
|
+
// All k should be in-bounds in the fast section
|
|
752
|
+
sum0 += src[k] * filter.coeffs[c - j];
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (add) {
|
|
756
|
+
dst[i] += sum0;
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
dst[i] = sum0;
|
|
760
|
+
}
|
|
761
|
+
// Odd output sample (i+1)
|
|
762
|
+
let sum1 = 0;
|
|
763
|
+
for (let j = -c; j <= c; j++) {
|
|
764
|
+
const upIdx = (i + 1) + p + j;
|
|
765
|
+
if ((upIdx & 1) === 0) {
|
|
766
|
+
const k = upIdx >> 1;
|
|
767
|
+
sum1 += src[k] * filter.coeffs[c - j];
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (add) {
|
|
771
|
+
dst[i + 1] += sum1;
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
dst[i + 1] = sum1;
|
|
775
|
+
}
|
|
776
|
+
i += 2;
|
|
777
|
+
}
|
|
778
|
+
// ── Section 3: Slow end (remaining samples from i to dstLen-1) ──
|
|
779
|
+
// Uses rightBound for ALL out-of-bounds edge extension
|
|
780
|
+
while (i < dstLen) {
|
|
781
|
+
let sum = 0;
|
|
782
|
+
for (let j = -c; j <= c; j++) {
|
|
783
|
+
const upIdx = i + p + j;
|
|
784
|
+
if ((upIdx & 1) === 0) {
|
|
785
|
+
const k = upIdx >> 1;
|
|
786
|
+
const sample = edgeExtend(src, k, srcLen, rightBound);
|
|
787
|
+
sum += sample * filter.coeffs[c - j];
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (add) {
|
|
791
|
+
dst[i] += sum;
|
|
792
|
+
}
|
|
793
|
+
else {
|
|
794
|
+
dst[i] = sum;
|
|
795
|
+
}
|
|
796
|
+
i++;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// ─── Wavelet reconstruction ────────────────────────────────────────────────
|
|
801
|
+
// From Ghidra wavelet_reconstruct_level @ 004bc640:
|
|
802
|
+
// param_1 = filter1 (analysis high, parity 0) → used in wavelet_filter_apply (SET)
|
|
803
|
+
// param_2 = filter2 (synthesis high, parity 1) → used in wavelet_filter_add (ADD)
|
|
804
|
+
//
|
|
805
|
+
// Vertical pass (direction=1, iterate over columns):
|
|
806
|
+
// tmpEven = filter1 * LL + filter2 * LH (column-by-column)
|
|
807
|
+
// tmpOdd = filter1 * HL + filter2 * HH (column-by-column)
|
|
808
|
+
//
|
|
809
|
+
// Horizontal pass (direction=0, iterate over rows):
|
|
810
|
+
// output = filter1 * tmpEven + filter2 * tmpOdd (row-by-row)
|
|
811
|
+
//
|
|
812
|
+
// The param5/param6 arguments to wavelet_filter_apply/add control edge extension:
|
|
813
|
+
// height even: apply(f1, dst, src, 1, 1, 2) then add(f2, dst, src, 1, 2, 1)
|
|
814
|
+
// height odd: apply(f1, dst, src, 1, 1, 1) then add(f2, dst, src, 1, 2, 2)
|
|
815
|
+
// width even: apply(f1, dst, src, 0, 1, 2) then add(f2, dst, src, 0, 2, 1)
|
|
816
|
+
// width odd: apply(f1, dst, src, 0, 1, 1) then add(f2, dst, src, 0, 2, 2)
|
|
817
|
+
function waveletReconstructLevel(filter1, filter2, output, level) {
|
|
818
|
+
const outW = output.width;
|
|
819
|
+
const outH = output.height;
|
|
820
|
+
const [evenW, oddW] = splitEvenOdd(outW);
|
|
821
|
+
const [evenH, oddH] = splitEvenOdd(outH);
|
|
822
|
+
const ll = level.subbands[0]; // LL: evenW × evenH
|
|
823
|
+
const lh = level.subbands[1]; // LH: evenW × oddH
|
|
824
|
+
const hl = level.subbands[2]; // HL: oddW × evenH
|
|
825
|
+
const hh = level.subbands[3]; // HH: oddW × oddH
|
|
826
|
+
// Determine edge extension params based on parity
|
|
827
|
+
const hEven = (outH & 1) === 0;
|
|
828
|
+
const wEven = (outW & 1) === 0;
|
|
829
|
+
const vLowP4 = 1, vLowP5 = hEven ? 2 : 1;
|
|
830
|
+
const vHighP4 = 2, vHighP5 = hEven ? 1 : 2;
|
|
831
|
+
const hLowP4 = 1, hLowP5 = wEven ? 2 : 1;
|
|
832
|
+
const hHighP4 = 2, hHighP5 = wEven ? 1 : 2;
|
|
833
|
+
// Temporary buffers for vertical pass results
|
|
834
|
+
const tmpEven = matrixCreate(evenW, outH);
|
|
835
|
+
const tmpOdd = matrixCreate(oddW, outH);
|
|
836
|
+
// ── Step 1: Vertical pass (column by column) ──
|
|
837
|
+
// tmpEven columns: filter1 * LL_col + filter2 * LH_col
|
|
838
|
+
for (let x = 0; x < evenW; x++) {
|
|
839
|
+
const col = new Float32Array(outH);
|
|
840
|
+
const llCol = new Float32Array(evenH);
|
|
841
|
+
for (let y = 0; y < evenH; y++)
|
|
842
|
+
llCol[y] = matrixGet(ll, x, y);
|
|
843
|
+
polyphaseConvolve1D(filter1, col, outH, llCol, evenH, vLowP4, vLowP5, false);
|
|
844
|
+
const lhCol = new Float32Array(oddH);
|
|
845
|
+
for (let y = 0; y < oddH; y++)
|
|
846
|
+
lhCol[y] = matrixGet(lh, x, y);
|
|
847
|
+
polyphaseConvolve1D(filter2, col, outH, lhCol, oddH, vHighP4, vHighP5, true);
|
|
848
|
+
for (let y = 0; y < outH; y++)
|
|
849
|
+
tmpEven.data[y * evenW + x] = col[y];
|
|
850
|
+
}
|
|
851
|
+
// tmpOdd columns: filter1 * HL_col + filter2 * HH_col
|
|
852
|
+
for (let x = 0; x < oddW; x++) {
|
|
853
|
+
const col = new Float32Array(outH);
|
|
854
|
+
const hlCol = new Float32Array(evenH);
|
|
855
|
+
for (let y = 0; y < evenH; y++)
|
|
856
|
+
hlCol[y] = matrixGet(hl, x, y);
|
|
857
|
+
polyphaseConvolve1D(filter1, col, outH, hlCol, evenH, vLowP4, vLowP5, false);
|
|
858
|
+
const hhCol = new Float32Array(oddH);
|
|
859
|
+
for (let y = 0; y < oddH; y++)
|
|
860
|
+
hhCol[y] = matrixGet(hh, x, y);
|
|
861
|
+
polyphaseConvolve1D(filter2, col, outH, hhCol, oddH, vHighP4, vHighP5, true);
|
|
862
|
+
for (let y = 0; y < outH; y++)
|
|
863
|
+
tmpOdd.data[y * oddW + x] = col[y];
|
|
864
|
+
}
|
|
865
|
+
// ── Step 2: Horizontal pass (row by row) ──
|
|
866
|
+
// output rows: filter1 * tmpEven_row + filter2 * tmpOdd_row
|
|
867
|
+
for (let y = 0; y < outH; y++) {
|
|
868
|
+
const row = new Float32Array(outW);
|
|
869
|
+
const evenRow = new Float32Array(evenW);
|
|
870
|
+
for (let x = 0; x < evenW; x++)
|
|
871
|
+
evenRow[x] = tmpEven.data[y * evenW + x];
|
|
872
|
+
polyphaseConvolve1D(filter1, row, outW, evenRow, evenW, hLowP4, hLowP5, false);
|
|
873
|
+
const oddRow = new Float32Array(oddW);
|
|
874
|
+
for (let x = 0; x < oddW; x++)
|
|
875
|
+
oddRow[x] = tmpOdd.data[y * oddW + x];
|
|
876
|
+
polyphaseConvolve1D(filter2, row, outW, oddRow, oddW, hHighP4, hHighP5, true);
|
|
877
|
+
for (let x = 0; x < outW; x++)
|
|
878
|
+
output.data[y * outW + x] = row[x];
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function waveletReconstructAll(filter1, filter2, pyramid, width, height) {
|
|
882
|
+
// From Ghidra wavelet_reconstruct_all @ 004bd1e0:
|
|
883
|
+
// Reconstruct from deepest level upward.
|
|
884
|
+
// At each level, the reconstruction output replaces the LL of the parent level.
|
|
885
|
+
// Level N-1 → output → LL of level N-2
|
|
886
|
+
// Level N-2 → output → LL of level N-3
|
|
887
|
+
// ...
|
|
888
|
+
// Level 0 → final output (full image size)
|
|
889
|
+
for (let lev = pyramid.numLevels - 1; lev >= 0; lev--) {
|
|
890
|
+
const level = pyramid.levels[lev];
|
|
891
|
+
// The output size = LL.width + HL.width × LL.height + LH.height
|
|
892
|
+
// which equals the original dimensions at this decomposition level.
|
|
893
|
+
const outW = level.subbands[0].width + level.subbands[2].width;
|
|
894
|
+
const outH = level.subbands[0].height + level.subbands[1].height;
|
|
895
|
+
const output = matrixCreate(outW, outH);
|
|
896
|
+
waveletReconstructLevel(filter1, filter2, output, level);
|
|
897
|
+
if (lev > 0) {
|
|
898
|
+
// Feed output as LL of parent level
|
|
899
|
+
pyramid.levels[lev - 1].subbands[0] = output;
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
return output;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return pyramid.levels[0].subbands[0]; // unreachable
|
|
906
|
+
}
|
|
907
|
+
// ─── Band size calculation ────────────────────────────────────────────────
|
|
908
|
+
function calcBandSize(width, height, quant, orientation) {
|
|
909
|
+
// From Ghidra disassembly (missed by decompiler):
|
|
910
|
+
// orientation==0: ceil(width / (quant*2)) * height * 2
|
|
911
|
+
// orientation==1: ceil(height / (quant*2)) * width * 2
|
|
912
|
+
// The *2 comes from FMUL DAT_004ed128 (=2.0) — accounts for interleaved block pairs
|
|
913
|
+
if (orientation === 0) {
|
|
914
|
+
return Math.ceil(width / (quant * 2)) * height * 2;
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
return Math.ceil(height / (quant * 2)) * width * 2;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
// ─── calc_bit_length ─────────────────────────────────────────────────────
|
|
921
|
+
// FUN_004b6ae0(bandValue, quant, rankTable):
|
|
922
|
+
// quant >= 2: calc_rank_bit_length(rankTable, quant, bandValue, 0) → bit length from RANK table
|
|
923
|
+
// quant < 2: bandValue * 2 + 1
|
|
924
|
+
// FUN_004b6b10: ceil(log2(result)) — but only for quant < 2 (for quant >= 2, rank table already stores bits)
|
|
925
|
+
// calc_bit_length: ceil(bitCount * 0.125) = bytes
|
|
926
|
+
function getRankBitLength(bandValue, quant, rankTable) {
|
|
927
|
+
// This is FUN_004b6ae0: returns the total number of states (for quant < 2)
|
|
928
|
+
// or the bit length directly from the rank table (for quant >= 2)
|
|
929
|
+
if (quant >= 2) {
|
|
930
|
+
return tableLookup(rankTable, quant, bandValue);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
return bandValue * 2 + 1;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
function calcBitLength(bandValue, quant, rankTable) {
|
|
937
|
+
let bitCount;
|
|
938
|
+
if (quant >= 2) {
|
|
939
|
+
// Rank table already stores bit counts
|
|
940
|
+
bitCount = getRankBitLength(bandValue, quant, rankTable);
|
|
941
|
+
if (bitCount < 0)
|
|
942
|
+
bitCount = 0;
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
// For quant < 2: number of states = bandValue * 2 + 1
|
|
946
|
+
// FUN_004b6b10 computes ceil(log2(states))
|
|
947
|
+
const states = getRankBitLength(bandValue, quant, rankTable);
|
|
948
|
+
bitCount = states <= 1 ? 0 : Math.ceil(Math.log2(states));
|
|
949
|
+
}
|
|
950
|
+
return Math.ceil(bitCount * DAT_004ed130);
|
|
951
|
+
}
|
|
952
|
+
// ─── level_scale_factor ─────────────────────────────────────────────────
|
|
953
|
+
function levelScaleFactor(extraBits) {
|
|
954
|
+
return (DAT_004ed1d0 - extraBits) * DAT_004ed1d8;
|
|
955
|
+
}
|
|
956
|
+
// ─── Block copy (FUN_004b6ba0) ──────────────────────────────────────────
|
|
957
|
+
function blockCopy(dst, src, srcLen, startX, startY, strideX, strideY) {
|
|
958
|
+
let x = startX, y = startY;
|
|
959
|
+
for (let i = 0; i < srcLen; i++) {
|
|
960
|
+
if (x < dst.width && y < dst.height && x >= 0 && y >= 0) {
|
|
961
|
+
matrixSet(dst, x, y, src[i]);
|
|
962
|
+
}
|
|
963
|
+
x += strideX;
|
|
964
|
+
y += strideY;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// ─── itw_decode_band ───────────────────────────────────────────────────────
|
|
968
|
+
function itwDecodeBand(dst, cursor, quant, bandValue, bandScale, orientation, bandOffset, diffTable, // exact counts T(q,m) — passed to fischerDecode for unranking
|
|
969
|
+
rankTable, // hardcoded bit lengths — used for reading codeword bits
|
|
970
|
+
version) {
|
|
971
|
+
const bandSize = calcBandSize(dst.width, dst.height, quant, orientation);
|
|
972
|
+
if (bandSize === 0)
|
|
973
|
+
return;
|
|
974
|
+
const positions = new Int32Array(bandSize);
|
|
975
|
+
const magnitudes = new Int32Array(bandSize);
|
|
976
|
+
const extraBits = new Int32Array(bandSize);
|
|
977
|
+
if (quant < 2) {
|
|
978
|
+
// Simple path: direct codeword per position
|
|
979
|
+
const bytesPerPos = calcBitLength(bandValue, quant, rankTable);
|
|
980
|
+
const bufSize = DAT_004ed11c * bandSize;
|
|
981
|
+
const inflated = cursor.copyStreamData(bufSize);
|
|
982
|
+
const bs = new Bitstream(inflated);
|
|
983
|
+
for (let i = 0; i < bandSize; i++) {
|
|
984
|
+
positions[i] = bs.readBits(bytesPerPos * 8);
|
|
985
|
+
}
|
|
986
|
+
// Reconstruct coefficients (quant1 path)
|
|
987
|
+
const range = bandValue * 2 + 1;
|
|
988
|
+
const scale = (bandScale / bandValue) * bandOffset; // band_offset_scale
|
|
989
|
+
// Wait, looking at coeff_reconstruct_dispatch: for quant==1:
|
|
990
|
+
// coeff_reconstruct_quant1(dst, positions, band_value, band_scale, band_offset, band_offset_scale)
|
|
991
|
+
// But the args passed are: band_value=bandValue, band_scale=bandScale, band_offset=0.0 (reserved_zero in the call)
|
|
992
|
+
// Actually re-reading itw_decode_main:
|
|
993
|
+
// itw_decode_band(view, cursor, quant, quantSteps, bandScale, orientation, 0, bandOffset, ...)
|
|
994
|
+
// And coeff_reconstruct_dispatch(dst, pos, mag, extra, quant, bandValue, bandScale, orientation, 0.0, bandOffset, rankTable)
|
|
995
|
+
// coeff_reconstruct_quant1(dst, pos, bandValue, bandScale, param_9=0.0, param_10=bandOffset)
|
|
996
|
+
// formula: (codeword % (bandValue*2+1) - bandValue) * (bandScale / bandValue) * bandOffset + 0.0
|
|
997
|
+
// Wait, the args are: param_4=bandScale, param_5=param_9=0.0, param_6=param_10=bandOffset
|
|
998
|
+
// So: value = (codeword % range - bandValue) * (bandScale / bandValue) * bandOffset + 0.0
|
|
999
|
+
// Actually looking more carefully at coeff_reconstruct_quant1 signature:
|
|
1000
|
+
// coeff_reconstruct_quant1(dst, pos_table, band_value, band_scale, band_offset, band_offset_scale)
|
|
1001
|
+
// And in itw_decode_main call to coeff_reconstruct_dispatch:
|
|
1002
|
+
// (dst, pos, mag, extra, quant, bandValue, bandScale, orientation, 0.0, bandOffset, rankTable)
|
|
1003
|
+
// coeff_reconstruct_dispatch maps: param_7=bandScale, param_9=0.0, param_10=bandOffset
|
|
1004
|
+
// For quant1: param_4=bandScale, param_5=0.0=band_offset, param_6=bandOffset=band_offset_scale
|
|
1005
|
+
// So: value = (codeword % range - bandValue) * (bandScale / bandValue) * bandOffset + 0.0
|
|
1006
|
+
// Hmm that seems off. Let me re-check.
|
|
1007
|
+
// coeff_reconstruct_quant1(param_1=dst, param_2=pos_table, param_3=band_value, param_4=band_scale, param_5=band_offset, param_6=band_offset_scale)
|
|
1008
|
+
// formula: (codeword % (param_3*2+1) - param_3) * (param_4 / param_3) * param_6 + param_5
|
|
1009
|
+
// So: (codeword % range - bandValue) * (bandScale / bandValue) * bandOffset + 0.0
|
|
1010
|
+
const w = dst.width;
|
|
1011
|
+
const h = dst.height;
|
|
1012
|
+
let posIdx = 0;
|
|
1013
|
+
for (let x = 0; x < w; x++) {
|
|
1014
|
+
for (let y = 0; y < h; y++) {
|
|
1015
|
+
if (posIdx < bandSize) {
|
|
1016
|
+
const codeword = positions[posIdx++];
|
|
1017
|
+
const val = (codeword % range - bandValue) * (bandScale / bandValue) * bandOffset;
|
|
1018
|
+
matrixSet(dst, x, y, val);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
// Fischer path: magnitude-based coding
|
|
1025
|
+
// Read magnitudes
|
|
1026
|
+
const magInflated = cursor.copyStreamData(bandSize);
|
|
1027
|
+
for (let i = 0; i < bandSize; i++) {
|
|
1028
|
+
magnitudes[i] = magInflated[i];
|
|
1029
|
+
}
|
|
1030
|
+
// Read extra bits (version 0 only)
|
|
1031
|
+
if (version === 0) {
|
|
1032
|
+
const bsData = new Uint8Array(cursor.remaining());
|
|
1033
|
+
const bs = new Bitstream(bsData);
|
|
1034
|
+
for (let i = 0; i < bandSize; i++) {
|
|
1035
|
+
if ((magnitudes[i] & DAT_004ed118) !== 0) {
|
|
1036
|
+
extraBits[i] = bs.readBits(4);
|
|
1037
|
+
}
|
|
1038
|
+
else {
|
|
1039
|
+
extraBits[i] = 0;
|
|
1040
|
+
}
|
|
1041
|
+
magnitudes[i] = magnitudes[i] & (DAT_004ed118 - 1); // mask off flag bit
|
|
1042
|
+
}
|
|
1043
|
+
// Update cursor position
|
|
1044
|
+
cursor.pos += bs.bytePos;
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
// version != 0: no extra bits
|
|
1048
|
+
extraBits.fill(0);
|
|
1049
|
+
}
|
|
1050
|
+
// Read codewords
|
|
1051
|
+
const cwBufSize = DAT_004ed11c * bandSize;
|
|
1052
|
+
const cwInflated = cursor.copyStreamData(cwBufSize);
|
|
1053
|
+
const cwBs = new Bitstream(cwInflated);
|
|
1054
|
+
for (let i = 0; i < bandSize; i++) {
|
|
1055
|
+
const bits = tableLookup(rankTable, quant, magnitudes[i]);
|
|
1056
|
+
positions[i] = bits > 0 ? cwBs.readBits(bits) : 0;
|
|
1057
|
+
}
|
|
1058
|
+
// Reconstruct coefficients (quant2 path)
|
|
1059
|
+
// From Ghidra coeff_reconstruct_quant2:
|
|
1060
|
+
// fVar8 = (band_scale / band_value) * band_offset_scale — stored as float32
|
|
1061
|
+
// sf = level_scale_factor(extraBits) — stored as float32
|
|
1062
|
+
// ratio = fVar8 / sf — stored as float32
|
|
1063
|
+
// fVar9 = decoded_int * ratio + band_offset — stored as float32
|
|
1064
|
+
// From itw_decode_main dispatch: band_offset = 0.0, band_offset_scale = bandOffset
|
|
1065
|
+
// CRITICAL: All intermediate values are truncated to float32 in the original x87 code.
|
|
1066
|
+
const fVar8 = Math.fround(Math.fround(bandScale / bandValue) * bandOffset);
|
|
1067
|
+
let posIdx = 0;
|
|
1068
|
+
if (orientation === 1) {
|
|
1069
|
+
// Vertical blocks: outer by Y in steps of quant*2, inner by X
|
|
1070
|
+
// Each pair: block at (bx, by) stride (0,2) and block at (bx, by+1) stride (0,2)
|
|
1071
|
+
for (let by = 0; by < dst.height; by += quant * 2) {
|
|
1072
|
+
for (let bx = 0; bx < dst.width; bx++) {
|
|
1073
|
+
// First block: write at (bx, by), (bx, by+2), (bx, by+4), ...
|
|
1074
|
+
if (posIdx < bandSize) {
|
|
1075
|
+
const sf = Math.fround(levelScaleFactor(extraBits[posIdx]));
|
|
1076
|
+
const ratio = Math.fround(fVar8 / sf);
|
|
1077
|
+
const decoded = fischerDecode(quant, positions[posIdx], magnitudes[posIdx], diffTable);
|
|
1078
|
+
for (let k = 0; k < quant; k++) {
|
|
1079
|
+
const y = by + k * 2;
|
|
1080
|
+
if (y < dst.height) {
|
|
1081
|
+
matrixSet(dst, bx, y, Math.fround(decoded[k] * ratio));
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
posIdx++;
|
|
1085
|
+
}
|
|
1086
|
+
// Second block: write at (bx, by+1), (bx, by+3), (bx, by+5), ...
|
|
1087
|
+
if (posIdx < bandSize) {
|
|
1088
|
+
const sf = Math.fround(levelScaleFactor(extraBits[posIdx]));
|
|
1089
|
+
const ratio = Math.fround(fVar8 / sf);
|
|
1090
|
+
const decoded = fischerDecode(quant, positions[posIdx], magnitudes[posIdx], diffTable);
|
|
1091
|
+
for (let k = 0; k < quant; k++) {
|
|
1092
|
+
const y = by + 1 + k * 2;
|
|
1093
|
+
if (y < dst.height) {
|
|
1094
|
+
matrixSet(dst, bx, y, Math.fround(decoded[k] * ratio));
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
posIdx++;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
// Horizontal blocks (orientation == 0): outer by X in steps of quant*2, inner by Y
|
|
1104
|
+
for (let bx = 0; bx < dst.width; bx += quant * 2) {
|
|
1105
|
+
for (let by = 0; by < dst.height; by++) {
|
|
1106
|
+
// First block: write at (bx, by), (bx+2, by), (bx+4, by), ...
|
|
1107
|
+
if (posIdx < bandSize) {
|
|
1108
|
+
const sf = Math.fround(levelScaleFactor(extraBits[posIdx]));
|
|
1109
|
+
const ratio = Math.fround(fVar8 / sf);
|
|
1110
|
+
const decoded = fischerDecode(quant, positions[posIdx], magnitudes[posIdx], diffTable);
|
|
1111
|
+
for (let k = 0; k < quant; k++) {
|
|
1112
|
+
const x = bx + k * 2;
|
|
1113
|
+
if (x < dst.width) {
|
|
1114
|
+
matrixSet(dst, x, by, Math.fround(decoded[k] * ratio));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
posIdx++;
|
|
1118
|
+
}
|
|
1119
|
+
// Second block: write at (bx+1, by), (bx+3, by), (bx+5, by), ...
|
|
1120
|
+
if (posIdx < bandSize) {
|
|
1121
|
+
const sf = Math.fround(levelScaleFactor(extraBits[posIdx]));
|
|
1122
|
+
const ratio = Math.fround(fVar8 / sf);
|
|
1123
|
+
const decoded = fischerDecode(quant, positions[posIdx], magnitudes[posIdx], diffTable);
|
|
1124
|
+
for (let k = 0; k < quant; k++) {
|
|
1125
|
+
const x = bx + 1 + k * 2;
|
|
1126
|
+
if (x < dst.width) {
|
|
1127
|
+
matrixSet(dst, x, by, Math.fround(decoded[k] * ratio));
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
posIdx++;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// ─── Read LL band ──────────────────────────────────────────────────────────
|
|
1138
|
+
// From Ghidra read_ll_band: outer loop = width (x), inner loop = height (y)
|
|
1139
|
+
// The bytes are stored column-major in the stream.
|
|
1140
|
+
function readLLBand(cursor, matrix) {
|
|
1141
|
+
for (let x = 0; x < matrix.width; x++) {
|
|
1142
|
+
for (let y = 0; y < matrix.height; y++) {
|
|
1143
|
+
const byte = cursor.readByte();
|
|
1144
|
+
matrixSet(matrix, x, y, byte);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
// ─── Subband view mapping ──────────────────────────────────────────────────
|
|
1149
|
+
// Maps the flat subband view array to pyramid subbands.
|
|
1150
|
+
// From itw_decode_main:
|
|
1151
|
+
// views[0] = level[0].subband[0] (LL at level 0)
|
|
1152
|
+
// views[1] = level[0].subband[1] (LH at level 0)
|
|
1153
|
+
// For each level i (1..numLevels-1):
|
|
1154
|
+
// views[2 + (i-1)*3 + 0] = level[i].subband[0] (LL)
|
|
1155
|
+
// views[2 + (i-1)*3 + 1] = level[i].subband[1] (LH)
|
|
1156
|
+
// views[2 + (i-1)*3 + 2] = level[i].subband[2] (HL)
|
|
1157
|
+
// views[local_a0] = level[numLevels-1].subband[3] (HH at deepest)
|
|
1158
|
+
// Actually wait, looking at the code again more carefully:
|
|
1159
|
+
// Level 0: band 0 (LL), band 1 (LH)
|
|
1160
|
+
// Level 1..N-1: band 0 (LL), band 1 (LH), band 2 (HL)
|
|
1161
|
+
// Last: HH of deepest level
|
|
1162
|
+
//
|
|
1163
|
+
// But the ORIENTATION and quant arrays are only for "detail" bands (local_a0 elements).
|
|
1164
|
+
// views has local_90 elements total, with the LAST one (views[local_a0]) being the LL of deepest.
|
|
1165
|
+
// Actually re-reading: piVar11 = piVar9 + local_a0 points to the last view slot,
|
|
1166
|
+
// and it's set to level[numLevels-1].subband[3] = HH of deepest level.
|
|
1167
|
+
// Then read_ll_band writes to this last view.
|
|
1168
|
+
//
|
|
1169
|
+
// WAIT. The code says:
|
|
1170
|
+
// piVar11 = (int *)pyramid_get_level(piVar8, iVar22 - 1); [deepest level]
|
|
1171
|
+
// piVar12 = level_get_subband(piVar11, 3); [HH of deepest]
|
|
1172
|
+
// piVar11 = piVar9 + local_a0; [last view slot]
|
|
1173
|
+
// *piVar11 = matrix_create_view(piVar12)
|
|
1174
|
+
// Then later: read_ll_band(cursor, *(*piVar11 + 4))
|
|
1175
|
+
// So the last view (index local_a0) is the HH of the deepest level,
|
|
1176
|
+
// and the LL band is read INTO that?? That doesn't make sense.
|
|
1177
|
+
// Unless... level_get_subband(level, 3) for the deepest level IS the LL band.
|
|
1178
|
+
// Let me re-check what subband index 3 is...
|
|
1179
|
+
// In the pyramid, level[numLevels-1] has subbands for the coarsest decomposition.
|
|
1180
|
+
// The "LL" at the very deepest level is a special case — it's the DC coefficients.
|
|
1181
|
+
// In a typical wavelet codec, the deepest LL is stored separately.
|
|
1182
|
+
// Here, subband 3 of the deepest level might be repurposed as the LL storage.
|
|
1183
|
+
// This is getting complicated. Let me just use a simpler mapping:
|
|
1184
|
+
// For the subband views that get decoded by itw_decode_band:
|
|
1185
|
+
// - views 0..(local_a0-1) are the detail subbands
|
|
1186
|
+
// - view local_a0 is the LL of the deepest level (stored as subband[3])
|
|
1187
|
+
// For 3 levels (local_90=9, local_a0=8):
|
|
1188
|
+
// views[0] = L0.LL, views[1] = L0.LH
|
|
1189
|
+
// views[2] = L1.LL, views[3] = L1.LH, views[4] = L1.HL
|
|
1190
|
+
// views[5] = L2.LL, views[6] = L2.LH, views[7] = L2.HL
|
|
1191
|
+
// views[8] = L2.HH (used for LL band reading)
|
|
1192
|
+
// For 4 levels (local_90=12, local_a0=11):
|
|
1193
|
+
// views[0] = L0.LL, views[1] = L0.LH
|
|
1194
|
+
// views[2] = L1.LL, ..., views[4] = L1.HL
|
|
1195
|
+
// views[5] = L2.LL, ..., views[7] = L2.HL
|
|
1196
|
+
// views[8] = L3.LL, ..., views[10] = L3.HL
|
|
1197
|
+
// views[11] = L3.HH
|
|
1198
|
+
// Quant steps from local_3c: [8, 8, 4, 4, 4, 2, 2, 2, 1, 1, 1]
|
|
1199
|
+
// For 3 levels, only first 8 are used (detail bands).
|
|
1200
|
+
// For 4 levels, all 11 are used.
|
|
1201
|
+
// ─── Main decode function ──────────────────────────────────────────────────
|
|
1202
|
+
function decode0300(buf, payloadOffset, width, height, opts) {
|
|
1203
|
+
// Read BE32 payload length, then the payload starts after it
|
|
1204
|
+
if (payloadOffset + 4 > buf.length)
|
|
1205
|
+
throw new itw_1.ITWError("missing wavelet length");
|
|
1206
|
+
const payloadLen = (0, itw_1.readBE32From2BE16)(buf, payloadOffset);
|
|
1207
|
+
const payloadStart = payloadOffset + 4;
|
|
1208
|
+
if (payloadStart + payloadLen > buf.length)
|
|
1209
|
+
throw new itw_1.ITWError("wavelet payload overruns file");
|
|
1210
|
+
// Build Fischer tables
|
|
1211
|
+
const baseTable = buildBaseTable(); // cumulative counts — used to build diff table
|
|
1212
|
+
const diffTable = buildDiffTable(baseTable); // exact counts T(q,m) — used by fischerDecode for unranking
|
|
1213
|
+
const rankTable = buildRankTable(); // hardcoded bit lengths — used for codeword reading
|
|
1214
|
+
// Quant step sizes per band
|
|
1215
|
+
const quantSteps = [8, 8, 4, 4, 4, 2, 2, 2, 1, 1, 1];
|
|
1216
|
+
// Read 3 header bytes from payload
|
|
1217
|
+
const cursor = new Cursor(buf, payloadStart);
|
|
1218
|
+
const version = cursor.readByte(); // DAT_00516c78
|
|
1219
|
+
const numLevels = cursor.readByte(); // 3 or 4
|
|
1220
|
+
const filterType = cursor.readByte(); // 0 or 1
|
|
1221
|
+
if (numLevels !== 3 && numLevels !== 4) {
|
|
1222
|
+
throw new itw_1.ITWError(`unsupported wavelet level count: ${numLevels}`);
|
|
1223
|
+
}
|
|
1224
|
+
const totalSubbands = numLevels === 3 ? 9 : 12; // local_90
|
|
1225
|
+
const detailSubbands = numLevels === 3 ? 8 : 11; // local_a0
|
|
1226
|
+
// Initialize wavelet filters (synthesis only needed for reconstruction)
|
|
1227
|
+
const { reconstructFilter1, reconstructFilter2 } = initFilters(filterType);
|
|
1228
|
+
// Debug: scale g1 (reconstructFilter2) coefficients by an override factor
|
|
1229
|
+
if (opts?.g1Scale !== undefined && opts.g1Scale !== 1.0) {
|
|
1230
|
+
const s = opts.g1Scale;
|
|
1231
|
+
reconstructFilter2.coeffs = reconstructFilter2.coeffs.map(c => c * s);
|
|
1232
|
+
}
|
|
1233
|
+
// Create wavelet pyramid
|
|
1234
|
+
const pyramid = pyramidCreate(width, height, numLevels);
|
|
1235
|
+
// Zero out level 0, band 2 in Ghidra = L0.HH in our mapping (subbands[3])
|
|
1236
|
+
// Ghidra subband order: [0]=HL, [1]=LH, [2]=HH, [3]=LL
|
|
1237
|
+
// Our subband order: [0]=LL, [1]=LH, [2]=HL, [3]=HH
|
|
1238
|
+
pyramid.levels[0].subbands[3].data.fill(0); // L0.HH zeroed
|
|
1239
|
+
// Build subband view mapping to match Ghidra's itw_decode_main:
|
|
1240
|
+
// Ghidra maps:
|
|
1241
|
+
// views[0] = level0.subband[0] = L0.HL → our subbands[2]
|
|
1242
|
+
// views[1] = level0.subband[1] = L0.LH → our subbands[1]
|
|
1243
|
+
// For levels 1+:
|
|
1244
|
+
// views[2+3*(i-1)+0] = Li.subband[0] = Li.HL → our subbands[2]
|
|
1245
|
+
// views[2+3*(i-1)+1] = Li.subband[1] = Li.LH → our subbands[1]
|
|
1246
|
+
// views[2+3*(i-1)+2] = Li.subband[2] = Li.HH → our subbands[3]
|
|
1247
|
+
// views[detailSubbands] = deepest_level.subband[3] = LL → our subbands[0]
|
|
1248
|
+
const views = [];
|
|
1249
|
+
views.push(pyramid.levels[0].subbands[2]); // L0.HL
|
|
1250
|
+
views.push(pyramid.levels[0].subbands[1]); // L0.LH
|
|
1251
|
+
for (let i = 1; i < numLevels; i++) {
|
|
1252
|
+
views.push(pyramid.levels[i].subbands[2]); // Li.HL
|
|
1253
|
+
views.push(pyramid.levels[i].subbands[1]); // Li.LH
|
|
1254
|
+
views.push(pyramid.levels[i].subbands[3]); // Li.HH
|
|
1255
|
+
}
|
|
1256
|
+
// The last view is the LL of deepest level — used as LL band target
|
|
1257
|
+
const llBandMatrix = pyramid.levels[numLevels - 1].subbands[0];
|
|
1258
|
+
views.push(llBandMatrix);
|
|
1259
|
+
// Per-frame loop (typically 1 iteration for param_1[5]=1)
|
|
1260
|
+
// The number of frames is stored in the image header at offset 0x14 (field [5]).
|
|
1261
|
+
// For our purposes, it's always 1 frame.
|
|
1262
|
+
const numFrames = 1;
|
|
1263
|
+
for (let frame = 0; frame < numFrames; frame++) {
|
|
1264
|
+
// Read orientation flags (1 bit per detail subband)
|
|
1265
|
+
const orientBsData = buf.subarray(cursor.pos);
|
|
1266
|
+
const orientBs = new Bitstream(orientBsData);
|
|
1267
|
+
const orientations = new Int32Array(detailSubbands);
|
|
1268
|
+
for (let i = 0; i < detailSubbands; i++) {
|
|
1269
|
+
orientations[i] = orientBs.readBits(1);
|
|
1270
|
+
}
|
|
1271
|
+
cursor.pos += orientBs.bytePos;
|
|
1272
|
+
// Read per-band parameters
|
|
1273
|
+
const bandQuantSteps = new Uint32Array(detailSubbands);
|
|
1274
|
+
const bandScales = new Float32Array(detailSubbands);
|
|
1275
|
+
const bandOffsets = new Float32Array(detailSubbands);
|
|
1276
|
+
for (let i = 0; i < detailSubbands; i++) {
|
|
1277
|
+
bandQuantSteps[i] = cursor.readBE16();
|
|
1278
|
+
const scaleQ15 = cursor.readBE16();
|
|
1279
|
+
bandScales[i] = q15ToFloat(scaleQ15);
|
|
1280
|
+
const offsetQ15 = cursor.readBE16();
|
|
1281
|
+
bandOffsets[i] = q15ToFloat(offsetQ15);
|
|
1282
|
+
}
|
|
1283
|
+
// Read min/max range
|
|
1284
|
+
const minVal = cursor.readBE16();
|
|
1285
|
+
const maxVal = cursor.readBE16();
|
|
1286
|
+
const range = maxVal - minVal;
|
|
1287
|
+
// Decode detail subbands
|
|
1288
|
+
for (let i = 0; i < detailSubbands; i++) {
|
|
1289
|
+
const view = views[i];
|
|
1290
|
+
itwDecodeBand(view, cursor, quantSteps[i], bandQuantSteps[i], bandScales[i], orientations[i], bandOffsets[i], diffTable, rankTable, version);
|
|
1291
|
+
}
|
|
1292
|
+
// Debug: zero out all detail bands to isolate LL-only reconstruction
|
|
1293
|
+
if (opts?.zeroDetailBands) {
|
|
1294
|
+
for (let i = 0; i < detailSubbands; i++) {
|
|
1295
|
+
views[i].data.fill(0);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// Debug: only keep specific detail bands (bandMask is a bitmask, bit i = keep band i)
|
|
1299
|
+
if (opts?.bandMask !== undefined) {
|
|
1300
|
+
for (let i = 0; i < detailSubbands; i++) {
|
|
1301
|
+
if ((opts.bandMask & (1 << i)) === 0) {
|
|
1302
|
+
views[i].data.fill(0);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// Debug: scale all detail bands by a gain factor
|
|
1307
|
+
if (opts?.detailGain !== undefined && opts.detailGain !== 1.0) {
|
|
1308
|
+
const g = opts.detailGain;
|
|
1309
|
+
for (let i = 0; i < detailSubbands; i++) {
|
|
1310
|
+
const d = views[i].data;
|
|
1311
|
+
for (let j = 0; j < d.length; j++)
|
|
1312
|
+
d[j] *= g;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
// Read LL band (raw bytes)
|
|
1316
|
+
readLLBand(cursor, llBandMatrix);
|
|
1317
|
+
// Post-process LL band: scale/clamp
|
|
1318
|
+
// From Ghidra decompilation + ASM of itw_decode_main (0x004b7e36-0x004b7e87):
|
|
1319
|
+
// uVar20 = read_be_multibyte(2) → first_val (min_val)
|
|
1320
|
+
// uVar16 = read_be_multibyte(2) → second_val (max_val)
|
|
1321
|
+
// fVar3 = (float)(uVar16 - uVar20) → range
|
|
1322
|
+
// local_44 = (fVar3 + (float)uVar20) * 0.5 → center [stored as float32]
|
|
1323
|
+
// FSUBRP at 004b7e7f: Intel opcode DE E1 = ST(1) ← ST(0) - ST(1)
|
|
1324
|
+
// FPU state: ST(0) = first_val, ST(1) = range
|
|
1325
|
+
// Result: first_val - range (POSITIVE for normal images)
|
|
1326
|
+
// llScale_raw = (first_val - range) * 0.5 [stored as float32]
|
|
1327
|
+
// llScale = (double)llScale_raw * (1.0/127.0)
|
|
1328
|
+
// value = (pixel - 127.0) * llScale + center
|
|
1329
|
+
// clamp: range ≤ value ≤ first_val
|
|
1330
|
+
//
|
|
1331
|
+
// For 26.ITW: first_val=3522, range=1705
|
|
1332
|
+
// center = (1705+3522)*0.5 = 2613.5
|
|
1333
|
+
// llScale_raw = (3522-1705)*0.5 = 908.5
|
|
1334
|
+
// pixel 0 → (0-127)*7.1535+2613.5 = 1705 (= range, lower bound) ✓
|
|
1335
|
+
// pixel 254 → (254-127)*7.1535+2613.5 = 3522 (= first_val, upper bound) ✓
|
|
1336
|
+
const centerF32 = Math.fround((range + minVal) * DAT_004ed190);
|
|
1337
|
+
const llScaleRawF32 = Math.fround((minVal - range) * 0.5); // first_val - range (POSITIVE)
|
|
1338
|
+
const llScale = llScaleRawF32 * DAT_004ed198; // float32 * double → double
|
|
1339
|
+
const llCenter = centerF32; // loaded from float32 → extended to double
|
|
1340
|
+
const clampLo = Math.min(minVal, range);
|
|
1341
|
+
const clampHi = Math.max(minVal, range);
|
|
1342
|
+
const llData = llBandMatrix.data;
|
|
1343
|
+
const llCount = llBandMatrix.width * llBandMatrix.height;
|
|
1344
|
+
for (let i = 0; i < llCount; i++) {
|
|
1345
|
+
let val = (llData[i] - DAT_004ed1a0) * llScale + llCenter;
|
|
1346
|
+
if (val <= clampLo)
|
|
1347
|
+
val = clampLo;
|
|
1348
|
+
else if (val >= clampHi)
|
|
1349
|
+
val = clampHi;
|
|
1350
|
+
llData[i] = val;
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
// Wavelet reconstruction
|
|
1354
|
+
const result = waveletReconstructAll(reconstructFilter1, reconstructFilter2, pyramid, width, height);
|
|
1355
|
+
// Convert float matrix to grayscale bytes
|
|
1356
|
+
// From disassembly of FUN_004b5b30:
|
|
1357
|
+
// CMP dword ptr [EDI],0x0 — compare float as integer (≤0 for negative/zero floats)
|
|
1358
|
+
// JLE → use 0
|
|
1359
|
+
// FCOM against 255.0 → clamp to min(val, 255.0)
|
|
1360
|
+
// CALL ftol → MSVC truncation toward zero (NOT round-to-nearest)
|
|
1361
|
+
// MOV byte ptr [EBX-1],AL — store as byte
|
|
1362
|
+
const pixels = new Uint8Array(width * height);
|
|
1363
|
+
for (let y = 0; y < height; y++) {
|
|
1364
|
+
for (let x = 0; x < width; x++) {
|
|
1365
|
+
let val = matrixGet(result, x, y);
|
|
1366
|
+
// Clamp float to [0, 255] THEN truncate (matching original ftol)
|
|
1367
|
+
if (val <= 0) {
|
|
1368
|
+
val = 0;
|
|
1369
|
+
}
|
|
1370
|
+
else if (val > 255) {
|
|
1371
|
+
val = 255;
|
|
1372
|
+
}
|
|
1373
|
+
pixels[y * width + x] = Math.trunc(val);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (opts?.returnFloat) {
|
|
1377
|
+
return { width, height, pixels, floatData: result.data };
|
|
1378
|
+
}
|
|
1379
|
+
return { width, height, pixels };
|
|
1380
|
+
}
|