a1800_codec 1.0.0

A clean room implementation of the GeneralPlus A1800 audio codec
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
# GeneralPlus A1800 Audio Codec — Technical Reference

Reverse-engineered from `A1800.DLL` (32-bit x86 Windows PE), a GeneralPlus proprietary audio codec. This document captures everything known (and unknown) about the codec from static analysis and decompilation.

---

## 1. Codec Overview

**Type**: Subband audio coder (NOT CELP, NOT transform/MDCT)

**Key parameters**:
- Sample rate: 16 kHz (presumed; see unknowns)
- Frame size: 320 samples (20 ms at 16 kHz)
- Supported bitrates: 4800–32000 bps in steps of 800
- Arithmetic: ITU-T G.729-style fixed-point (i16/i32 with saturation)
- Channels: Mono

**Architecture**: 5-stage butterfly filterbank splitting 320 time-domain samples into 32 subbands of 10 samples each. Only the first 8–14 subbands are coded (depending on bitrate); the rest are zeroed.

---

## 2. File Format (.a18)

```
Offset  Size  Type     Description
──────  ────  ────     ───────────
0x00    4     LE u32   data_length — total bytes of frame data after the header
0x04    2     LE u16   bitrate — e.g. 16000
0x06    ...   bytes    encoded frames, each (bitrate / 800) × 2 bytes
```

Each encoded frame is `bitrate / 800` 16-bit little-endian words. For example, at 16000 bps: 20 words = 40 bytes per frame.

---

## 3. Bitrate-Dependent Parameters

| Bitrate Range  | num_subbands | bits_per_frame | encoded_frame_size (i16 words) |
|----------------|-------------|----------------|-------------------------------|
| 4800–9599      | 8           | bitrate/50     | bitrate/800                   |
| 9600–11999     | 10          | "              | "                             |
| 12000–15999    | 12          | "              | "                             |
| 16000–32000    | 14          | "              | "                             |

Subbands beyond `num_subbands` are zeroed. Each subband contains 20 samples (10 samples × 2 subframes, or equivalently 20 interleaved).

---

## 4. Decode Pipeline

Per frame, the decode pipeline is:

```
Bitstream → Gain Decode → Bit Allocation → Subframe Decode → Inverse Filterbank → Synthesis
```

### 4.1. Gain Decode (`decode_gains` / DLL 0x10003050)

1. Read 5-bit initial gain index → `initial_gain = index - 7`
2. For each additional subband (up to num_subbands - 1):
   - Huffman-decode a differential using GAIN_HUFFMAN_TREE
   - Tree has 13 sections of 23 nodes each (one per subband differential)
   - Node index starts at section × 23; positive = child, negative/zero = leaf (negate for symbol)
3. Cumulative gains: `gain[i+1] = gain[i] + differential[i] - 12`
4. Compute scale_param (controls synthesis scaling exponent):
   - Start sp=9
   - Compute total_cost from SCALE_FACTOR_BITS, find max effective gain
   - Iteratively halve cost, reduce gain by 2, decrement sp until constraints met
5. Final scale factors: `scale_factor[i] = SCALE_FACTOR_BITS[gain[i] + sp*2 + 24]`

### 4.2. Read 4-bit Frame Parameter

A 4-bit value consumed from the bitstream after gains. Used to fine-tune bit allocation via `increment_allocation_bins`.

### 4.3. Bit Allocation (`compute_bit_alloc_for_frame` / DLL 0x10001d30)

Three-step process:

1. **Budget adjustment**: If remaining bits > 320, compress excess: `budget = ((remaining - 320) * 5) >> 3 + 320`

2. **Binary search for threshold** (`search_threshold` / DLL 0x100020f0):
   - Start threshold = -32, step = 32
   - For each subband: `alloc[i] = clamp((threshold - gain[i]) >> 1, 0, 7)`
   - Compute cost = sum of BIT_ALLOC_COST[alloc[i]]
   - If cost >= budget - 32, keep threshold
   - Halve step, repeat until step = 0

3. **Greedy optimization** (`optimize_allocation` / DLL 0x10001dc0):
   - 15 iterations of adjusting allocations up/down to balance cost against 2× budget
   - Under budget → decrease worst subband (smallest `threshold - gain - 2*step` metric)
   - Over budget → increase best subband (largest metric, scanning from top subband down)
   - Records operations in a swap log

4. **Apply frame parameter**: The 4-bit value indexes into the swap log to replay N increment operations.

### 4.4. Subframe Decode (`decode_subframes` / DLL 0x100032e0)

For each subband, based on its allocation step (0–7):

**Steps 0–4**: Pure codebook decode
- Huffman-decode a symbol from CODEBOOK_TREE_{0..6}
- Inverse quantize: decompose symbol into digits via iterated division
  - `quotient = mult(val, QUANT_STEP_SIZE[step])`
  - `remainder = val - quotient * (QUANT_INV_STEP[step] + 1)`
  - digit = remainder, then val = quotient; repeat for QUANT_LEVELS_M1[step] digits
- Read sign bits (one per nonzero digit)
- Reconstruct: `sample = extract_l(L_shr(L_mult0(scale_factor, QUANT_RECON_LEVELS[step][digit]), 12))`
- Apply sign: if sign bit = 0 → negate

**Steps 5–6**: Codebook decode + conditional noise fill
- Same codebook decode as above
- After decode, fill any remaining zero samples with shaped noise
- `noise_level = mult(scale_factor, NOISE_GAINS[step - 5])`
- NOISE_GAINS = [0x16A1, 0x2000, 0x5A82]
- Two PRNG calls per subband (one for first 10 samples, one for last 10)
- Each PRNG output bit determines noise sign (+noise_level or -noise_level)
- Only applied to samples that are still zero

**Step 7**: Full noise fill (no codebook decode)
- All 20 samples replaced with noise
- `noise_level = mult(scale_factor, NOISE_GAINS[2])` (= 0x5A82)
- Same PRNG-based sign selection

**Error handling**: If bitstream runs out mid-decode, remaining subbands are set to step 7 (noise-filled).

### 4.5. Noise PRNG (`noise_prng` / DLL 0x10003870)

4-tap linear feedback register:
```
sum = state[0] + state[3]
if sum is negative, add 1
shift state: [3] ← [2] ← [1] ← [0] ← sum
return sum
```
Initialized to [1, 1, 1, 1].

### 4.6. Inverse Filterbank (`inverse_filterbank` / DLL 0x10002740)

Three phases operating on 320 samples:

**Phase 1: 5-stage butterfly decomposition**

Each stage splits groups into sums (front) and differences (back):
- Stage 0 (1 group of 320): 32-bit precision
  - `sum = extract_l(L_shr(L_add(a, b), 1))`
  - `diff = extract_l(L_shr(L_add(a, -b), 1))`
- Stages 1–4 (2/4/8/16 groups): 16-bit precision
  - `sum = add(a, b)`
  - `diff = add(a, negate(b))`

Uses ping-pong between two 320-element scratch buffers.

**Phase 2: Cosine modulation**

32 groups of 10 samples, each multiplied by a shared 10×10 cosine matrix.

```
output[g*10 + k] = extract_h(L_shr(
    sum(j=0..9: L_mac(acc, butterfly[g*10+j], COSINE_MOD_MATRIX[k + j*10])),
    1))
```

Only the first 100 entries of COSINE_MOD_MATRIX are meaningful (the 10×10 matrix). The remaining 220 entries are small noise-like values (possibly padding/unrelated data in the DLL's .rdata).

**Phase 3: 5-stage reconstruction with filterbank coefficients**

Stages 4→0, each using a coefficient table (FILTERBANK_COEFF_0 through FILTERBANK_COEFF_4). Each stage processes groups with a 4-coefficient butterfly:

```
Given inputs a, b (first half) and c, d (second half), and coefficients c0–c3:
A = extract_h(L_shl(L_mac(L_mac(0, c0, a), negate(c1), c), 1))  → front
C = extract_h(L_shl(L_mac(L_mac(0, c2, b), c3, d), 1))          → front+1
B = extract_h(L_shl(L_mac(L_mac(0, c1, a), c0, c), 1))          → back-1
D = extract_h(L_shl(L_mac(L_mac(0, c3, b), negate(c2), d), 1))  → back-2
```

Outputs are placed in interleaved front/back order within each group.

Coefficient table sizes: 20, 40, 80, 160, 320 entries (4 coefficients per butterfly × half_group/2 iterations × 1 set reused across all groups in the stage).

| Stage | Group Size | Num Groups | Coeff Table          | Entries |
|-------|-----------|------------|----------------------|---------|
| 4     | 20        | 16         | FILTERBANK_COEFF_0   | 20      |
| 3     | 40        | 8          | FILTERBANK_COEFF_1   | 40      |
| 2     | 80        | 4          | FILTERBANK_COEFF_2   | 80      |
| 1     | 160       | 2          | FILTERBANK_COEFF_3   | 160     |
| 0     | 320       | 1          | FILTERBANK_COEFF_4   | 320     |

**Final scaling**: If frame_size == 320 (always true in practice), all output samples are shifted left by 1 via `shl(sample, 1)`.

### 4.7. Synthesis Filter (`synthesis_filter` / DLL 0x10001b60)

Windowed overlap-add producing 320 PCM samples from inverse filterbank output + 160-sample memory from previous frame.

1. Call inverse_filterbank → `filtered[0..319]`
2. Apply scale_param: if > 0, `shr(sample, scale_param)`; if < 0, `shl(sample, -scale_param)`
3. First 160 output samples:
   ```
   output[k] = extract_h(L_shl(
       L_mac(L_mac(0, SYNTH_OVERLAP[k], filtered[159-k]),
             SYNTH_OVERLAP[319-k], memory[k]),
       2))
   ```
4. Second 160 output samples:
   ```
   output[160+k] = extract_h(L_shl(
       L_mac(L_mac(0, SYNTH_OVERLAP[160+k], filtered[k]),
             negate(SYNTH_OVERLAP[159-k]), memory[159-k]),
       2))
   ```
5. Update memory: `memory[k] = filtered[160+k]` for k=0..159

Note: only `filtered[0..159]` is used in output computation; `filtered[160..319]` is saved as next frame's overlap memory. This is classic overlap-add where the current frame's second half contributes to the next frame.

---

## 5. Encode Pipeline

Per frame, the encode pipeline mirrors the decode pipeline in reverse:

```
PCM → Analysis Filter → Gain Encode → Bit Allocation → Subframe Encode → Bitstream Pack
```

### 5.1. Analysis Filter (`analysis_filter` / DLL 0x10004ba0)

Converts 320 PCM samples to 320 subband samples + returns `scale_param`:

1. **Windowed overlap** using ANALYSIS_WINDOW (320 entries at 0x10010718):
   - First 160 outputs: symmetric window applied to memory buffer
     `windowed[k] = extract_h(L_mac(L_mac(0, WINDOW[159-k], memory[159-k]), WINDOW[160+k], memory[160+k]))`
   - Second 160 outputs: window applied to pcm_input
     `windowed[160+k] = extract_h(L_mac(L_mac(0, WINDOW[319-k], pcm[k]), negate(WINDOW[k]), pcm[319-k]))`
2. Copy all 320 pcm_input samples into memory for next frame's overlap
3. **Compute scale_param** from max absolute windowed value:
   - If max_abs >= 14000 → sp = 0
   - Else: `adj = max_abs` (or +1 if < 0x1b6), `val = extract_l(L_shr(L_mult(adj, 0x2573), 0x14))`, `sp = norm_s(val) - 6` (or 9 if norm_s returns 0)
   - Sum-of-abs check: if `max_abs < L_shr(sum_of_abs, 7)`, decrement sp
4. **Apply scaling**: if sp > 0 → shl; if sp < 0 → shr
5. **Forward filterbank** to produce subbands

### 5.2. Forward Filterbank (`forward_filterbank` / DLL 0x10002280)

Same 3-phase structure as the inverse filterbank (butterfly→cosine→reconstruct) but with key differences:

**Phase 1: Butterfly stages 0→4**: All stages use 32-bit precision with pre-scaling to prevent overflow. Reads pairs sequentially, writes sums to front and differences to back:
```
a_shr = L_shr(a, 1);  b_shr = L_shr(b, 1)
front = extract_l(L_add(a_shr, b_shr))
back  = extract_l(L_sub(a_shr, b_shr))
```
vs. the inverse which reads front/back and writes sequentially, and uses 16-bit precision for stages 1-4.

**Phase 2: Cosine modulation**: Uses `FWD_COSINE_MOD_MATRIX` (0x1000bb88) and `extract_h(acc)` directly (no `L_shr(acc, 1)` as in the inverse).

**Phase 3: Reconstruction stages 4→0**: Uses `FWD_FILTERBANK_COEFF_PTRS` (0x1000bb70, mapping PTRS[0]→COEFF_0 for stage 4, through PTRS[4]→COEFF_4 for stage 0). Same 4-coefficient butterfly formula as the inverse but without the `L_shl(1)` wrapper — `extract_h(L_mac(...))` directly.

### 5.3. Gain Encode (`encode_gains` / DLL 0x100040b0)

1. For each subband, compute energy: `sum(sample² for 20 samples)` via `L_mac0`
2. Convert to log-scale gain index (normalize via leading-zero count)
3. Clamp first gain to [-6, 24], subsequent differentials to [-15, 24]
4. Huffman-encode differentials using `GAIN_HUFFMAN_BIT_WIDTHS` and `GAIN_HUFFMAN_CODES` tables
5. Returns total bits consumed by gain encoding

### 5.4. Encode Frame (`encode_frame` / DLL 0x10003ad0)

1. `encode_gains` → gain indices + Huffman codes
2. `compute_bit_alloc_for_frame` → per-subband allocation (same function as decoder)
3. `prescale_subbands` → normalize subband samples by gain (right-shift proportional to gain)
4. `encode_subframes` → quantize each subband via `forward_quantize`, pack into coded data
5. `write_bitstream` → assemble gain codes + 4-bit frame parameter + coded subbands into output words

### 5.5. Bitstream Packing (`write_bitstream` / DLL 0x10003c30)

Packs the following into 16-bit LE output words:
1. Per-subband gain Huffman codes (variable width)
2. 4-bit frame parameter (from optimization swap count)
3. Per-subband coded data: Huffman symbols + sign bits

Parameters: `(encoded_data, subband_bits, gain_codes, gain_bit_widths, output, frame_param_code, num_subbands, frame_param_bits=4, bits_per_frame)`

### 5.6. enc_frame / enc_frame_init Entry Points

```
enc_frame_init(bitrate, &enc_frame_words_out, &dec_frame_size_out) → 0=ok, 8=bad bitrate
    Calls a1800_enc_frame_init, then returns encoded frame word count and decoded frame size (320).

enc_frame(pcm_input, output_bitstream) → 0
    Calls analysis_filter(pcm, g_enc_filterbank_memory, subbands, 320) → scale_param
    Calls encode_frame(g_enc_bits_per_frame, g_enc_num_subbands, subbands, scale_param, output)
```

---

## 6. Constant Tables

All tables were extracted from the DLL's .rdata section via Ghidra memory inspection.

### Decoder Tables

| Table                 | Address      | Size      | Purpose                                      |
|-----------------------|-------------|-----------|----------------------------------------------|
| BIT_ALLOC_COST        | 0x1000d9f0  | 8 i16     | Cost in bits per quantizer step (0–7)        |
| SCALE_FACTOR_BITS     | 0x100105a8  | 128 i16   | Exponential power curve + plateau + mirror    |
| QUANT_LEVELS_M1       | 0x100106b8  | 8 i16     | Number of quantizer digits minus 1            |
| QUANT_NUM_COEFF       | 0x100106c8  | 8 i16     | Subframes per subband per quantizer step      |
| QUANT_INV_STEP        | 0x100106d8  | 8 i16     | Inverse quantizer step sizes                  |
| QUANT_STEP_SIZE       | 0x100106e8  | 8 i16     | Quantizer step sizes (Q15 reciprocals)        |
| QUANT_RECON_LEVELS    | 0x1000d8f0  | 8×16 i16  | Reconstruction levels per step                |
| GAIN_HUFFMAN_TREE     | 0x1000d3e8  | 300×2 i16 | 13 sections × 23 nodes, binary tree           |
| COSINE_MOD_MATRIX     | 0x1000bed0  | 320 i16   | First 100 = 10×10 cosine matrix; rest unused  |
| FILTERBANK_COEFF_0    | 0x1000C498  | 20 i16    | Inverse reconstruction stage 4 coefficients   |
| FILTERBANK_COEFF_1    | 0x1000C4C0  | 40 i16    | Inverse reconstruction stage 3 coefficients   |
| FILTERBANK_COEFF_2    | 0x1000C510  | 80 i16    | Inverse reconstruction stage 2 coefficients   |
| FILTERBANK_COEFF_3    | 0x1000C5B0  | 160 i16   | Inverse reconstruction stage 1 coefficients   |
| FILTERBANK_COEFF_4    | 0x1000C6F0  | 320 i16   | Inverse reconstruction stage 0 coefficients   |
| CODEBOOK_TREE_0..6    | various     | various   | 7 Huffman codebook trees for quantizer steps  |
| SYNTH_OVERLAP_OFFSETS | 0x10010998  | 320 i16   | Synthesis window coefficients (monotonic rise) |
| Coeff pointer table   | 0x1000ce70  | 6 ptrs    | Pointers to FILTERBANK_COEFF_0..5             |

### Encoder Tables

| Table                   | Address      | Size      | Purpose                                       |
|-------------------------|-------------|-----------|-----------------------------------------------|
| FWD_COSINE_MOD_MATRIX   | 0x1000bb88  | 100 i16   | Forward filterbank 10×10 cosine matrix        |
| FWD_FILTERBANK_COEFF_0  | 0x1000b198  | 20 i16    | Forward reconstruction stage 4                |
| FWD_FILTERBANK_COEFF_1  | 0x1000b1c0  | 40 i16    | Forward reconstruction stage 3                |
| FWD_FILTERBANK_COEFF_2  | 0x1000b210  | 80 i16    | Forward reconstruction stage 2                |
| FWD_FILTERBANK_COEFF_3  | 0x1000b2b0  | 160 i16   | Forward reconstruction stage 1                |
| FWD_FILTERBANK_COEFF_4  | 0x1000b3f0  | 320 i16   | Forward reconstruction stage 0                |
| FWD_FILTERBANK_COEFF_5  | 0x1000b670  | 640 i16   | Forward reconstruction extended (stage 0)     |
| GAIN_HUFFMAN_BIT_WIDTHS | 0x1000cea8  | 336 i16   | Gain differential Huffman widths (14×24)      |
| GAIN_HUFFMAN_CODES      | 0x1000d148  | 336 i16   | Gain differential Huffman codes (14×24)       |
| ANALYSIS_WINDOW         | 0x10010718  | 320 i16   | Analysis filter window coefficients           |
| QUANT_SCALE_FACTOR      | 0x100106a8  | 8 i16     | Forward quantizer scale factors               |
| QUANT_SCALE_BY_GAIN     | 0x10010628  | 64 i16    | Gain-to-scale multiplier lookup               |
| QUANT_ROUNDING          | 0x100106f8  | 8 i16     | Quantizer rounding offsets                    |
| FWD_CODEBOOK_CODES_0..6 | various     | various   | 7 Huffman code tables for forward quantizer   |
| FWD_CODEBOOK_WIDTHS_0..6| various     | various   | 7 Huffman width tables for forward quantizer  |

### SCALE_FACTOR_BITS Structure

128 entries with a distinctive pattern:
- Indices 0–21: all zeros
- Indices 22–53: exponential power curve (1, 1, 1, 1, 2, 3, 4, 6, ... 16384, 23170)
- Indices 54–63: zeros
- Indices 64–88: plateau at 32767
- Indices 89–127: descending mirror of the rising portion

### Codebook Trees

Flat arrays where `tree[node*2]` = left child, `tree[node*2+1]` = right child. Positive values are child node indices; negative/zero values are leaf symbols (negate to get the decoded symbol). Tree sizes vary: 360, 186, 94, 1038, 416, 382, 62 entries for steps 0–6 respectively.

---

## 7. Fixed-Point Arithmetic

All arithmetic matches ITU-T G.729 basic operations. Key functions:

| Function    | Signature            | Semantics                                     |
|-------------|---------------------|-----------------------------------------------|
| saturate    | i32 → i16           | Clamp to [-32768, 32767]                      |
| add         | (i16, i16) → i16    | Saturating 16-bit addition                    |
| sub         | (i16, i16) → i16    | Saturating 16-bit subtraction                 |
| negate      | i16 → i16           | Saturating negate (-32768 → 32767)            |
| abs_s       | i16 → i16           | Saturating absolute value                     |
| shl         | (i16, i16) → i16    | Left shift with overflow saturation           |
| shr         | (i16, i16) → i16    | Arithmetic right shift                        |
| mult        | (i16, i16) → i16    | Q15 multiply: (a*b) >> 15                     |
| L_mult      | (i16, i16) → i32    | a*b*2 with saturation for 0x40000000          |
| L_mac       | (i32, i16, i16) → i32 | acc + a*b*2                                |
| L_add       | (i32, i32) → i32    | Saturating 32-bit addition                    |
| L_sub       | (i32, i32) → i32    | Saturating 32-bit subtraction                 |
| L_shl       | (i32, i16) → i32    | 32-bit left shift with saturation             |
| L_shr       | (i32, i16) → i32    | 32-bit arithmetic right shift                 |
| extract_h   | i32 → i16           | High 16 bits (val >> 16)                      |
| extract_l   | i32 → i16           | Low 16 bits (val as i16)                      |
| L_deposit_l | i16 → i32           | Sign-extend 16-bit to 32-bit                  |
| norm_s      | i16 → i16           | Count leading redundant sign bits             |
| L_mult0     | (i16, i16) → i32    | a*b (no ×2)                                   |
| L_mac0      | (i32, i16, i16) → i32 | acc + a*b (no ×2)                           |

---

## 8. DLL Function Map

### Decoder Functions

| DLL Function                    | Address     | Rust Location                  |
|---------------------------------|------------|-------------------------------|
| a1800_dec_frame_init            | 0x10002ca0 | decoder.rs::DecoderState::new |
| a1800_dec_frame / dec_frame     | 0x10002e70 | decoder.rs::decode_frame_to_subbands |
| decode_frame_params             | 0x10002f60 | decoder.rs::decode_frame_params |
| decode_gains                    | 0x10003050 | decoder.rs::decode_gains      |
| read_bit                        | 0x10003820 | bitstream.rs::read_bit        |
| compute_bit_alloc_for_frame     | 0x10001d30 | decoder.rs::compute_bit_alloc_for_frame (shared with encoder) |
| search_bit_allocation_threshold | 0x100020f0 | decoder.rs::search_threshold (shared) |
| compute_bit_allocation          | 0x10002200 | decoder.rs::compute_allocation (shared) |
| optimize_bit_allocation         | 0x10001dc0 | decoder.rs::optimize_allocation (shared) |
| increment_allocation_bins       | 0x10003290 | decoder.rs::increment_allocation_bins (shared) |
| decode_subframes                | 0x100032e0 | decoder.rs::decode_subframes  |
| inverse_quantize                | 0x10003760 | decoder.rs::inverse_quantize  |
| noise_prng                      | 0x10003870 | decoder.rs::noise_prng        |
| inverse_filterbank              | 0x10002740 | filterbank.rs::inverse        |
| synthesis_filter                | 0x10001b60 | synthesis.rs::synthesize      |
| saturate...norm_s               | 0x100016e0–0x10001b20 | fixedpoint.rs     |

### Encoder Functions

| DLL Function                    | Address     | Rust Location                                 |
|---------------------------------|------------|-----------------------------------------------|
| a1800_enc_frame_init            | 0x100038c0 | encoder.rs::EncoderState::new                 |
| enc_frame                       | 0x100039d0 | encoder.rs::encode_frame_to_bitstream         |
| analysis_filter                 | 0x10004ba0 | analysis.rs::analysis_filter                  |
| forward_filterbank              | 0x10002280 | filterbank.rs::forward                        |
| encode_frame                    | 0x10003ad0 | encoder.rs::encode_frame                      |
| encode_gains                    | 0x100040b0 | encoder.rs::encode_gains                      |
| prescale_subbands               | 0x10003fe0 | encoder.rs::prescale_subbands                 |
| encode_subframes                | 0x100043e0 | encoder.rs::encode_subframes                  |
| forward_quantize                | 0x10004730 | encoder.rs::forward_quantize                  |
| write_bitstream                 | 0x10003c30 | encoder.rs::write_bitstream                   |

### DLL API Exports

| Export          | Address     | Signature                                                           |
|-----------------|------------|---------------------------------------------------------------------|
| a1800_enc       | 0x10001000 | (input_wav_path, output_a18_path, bitrate, output_info, progress_cb) → int |
| a1800_dec       | 0x10001370 | (src_path, dst_path, &bitrate, sample_rate, progress_cb) → int     |
| get_bitrate_info| 0x10001660 | (&num_bitrates_out, &bitrate_step_out) → ptr to BITRATE_TABLE      |
| get_bitrate     | 0x10001680 | (bitrate) → validated bitrate or 0                                 |
| get_err_str     | 0x100015d0 | (error_code) → error string pointer                                |

### WAV / CRT Helper Functions

| Function             | Address     | Description                                    |
|----------------------|------------|------------------------------------------------|
| wav_get_sample_rate  | 0x10004b30 | Find "fmt " chunk, read sample rate (u32 LE)  |
| wav_find_chunk       | 0x10004ad0 | Search RIFF chunks for matching chunk ID       |
| file_get_sample_count| 0x10004aa0 | Find "data" chunk, return byte_size / 2        |
| wav_header_init      | 0x100049e0 | Initialize 44-byte WAV header struct           |
| wav_header_set_params| 0x10004a70 | Set sample rate / format in header             |
| wav_header_update_size| 0x10004a50| Patch data size after encoding                 |
| crt_fopen            | 0x10005260 | fopen(filename, mode) — SH_DENYNO             |
| crt_fread            | 0x10004fd1 | fread(buf, elem_size, count, file)             |
| crt_fwrite           | 0x100050e8 | fwrite(buf, elem_size, count, file)            |
| crt_fseek            | 0x10004e90 | fseek(file, offset, whence)                    |
| crt_ftell            | 0x10005273 | ftell(file)                                    |

---

## 9. Decoder State

Persistent state across frames:
- `prng_state: [i16; 4]` — noise PRNG, initialized to [1, 1, 1, 1]
- `synth_memory: [i16; 320]` — only first 160 used for overlap-add, initialized to zeros
- `filterbank_memory: [i16; 640]` — allocated but **purpose unclear** (filterbank is stateless in our analysis; see unknowns)

---

## 10. Bugs Found During Reverse Engineering

### shl overflow condition (fixedpoint.rs)
The DLL at 0x10001780 has the condition `(shift < 16 || val == 0)` for the non-overflow path. An initial reading misinterpreted this as `shift < 16 && val != 0`, which caused `shl(0, 1)` to incorrectly return -32768 instead of 0.

### SCALE_FACTOR_BITS table size
Initially extracted as 32 entries. The gain decoder accesses indices up to ~53 (gain + scale_param * 2 + 24), which was out of bounds. Inspecting memory at 0x100105a8 revealed 128 entries forming a symmetric exponential power curve.

---

## 11. Things I Don't Know / Open Questions

### File Format
- **Is the .a18 header always exactly 6 bytes?** The 4-byte length + 2-byte bitrate was determined from one analysis path. There may be additional header fields in some variants.
- **Is the data_length field in bytes or some other unit?** We assume bytes based on context but haven't confirmed with multiple files.
- **Are there any other container formats** that embed A1800 frames? The DLL exports suggest it can work with raw frame buffers.

### Sample Rate
- **Is 16 kHz the only supported sample rate?** The DLL's decode function takes a sample rate parameter but the codec itself doesn't embed it in the bitstream. We default to 16 kHz. Other rates (8 kHz, 32 kHz) might be used with different frame sizes, or the same 320-sample frame at a different rate.
- **What frame sizes other than 320 are valid?** The filterbank has a `frame_size` parameter and special-cases 320 with a final ×2 scaling. Other sizes may exist but are untested.

### Encoder Side
- **The encoder is implemented in Rust** with a few remaining structural differences from the DLL (see Section 12). The analysis filter, forward filterbank, and forward quantizer now match the DLL. Round-trip works for bitrates 4800–24000 bps. At 32000 bps the encoder's prescaling can cause all-zero quantization on the first frame.
- **The 4-bit frame parameter** encodes the number of swap operations from `optimize_bit_allocation`'s swap log that should be replayed on the decoder side.
- **Encoder scale_param consistency**: The encoder computes scale_param from the analysis filter, but must also recompute it from gain indices using the decoder's algorithm (`compute_scale_param_from_gains`) so the encoder and decoder agree on the value.

### Bit Allocation
- **Why the budget cap at 320?** The budget adjustment formula `((excess - 320) * 5) >> 3 + 320` limits effective bits, but the rationale is unclear.
- **Why 15 optimization iterations (num_iterations=16, loop runs max_iter=15)?** This seems like a fixed constant but may relate to maximum meaningful adjustments.

### Decoder State
- **What is `filterbank_memory` (640 entries) used for?** The inverse filterbank uses only local stack buffers (three 320-element arrays). The 640-entry allocation in the decoder state may be:
  - Dead/unused (over-allocated in the DLL)
  - Used by a different code path not yet analyzed (e.g., error concealment, PLC)
  - Used by the encoder side
- **Is `synth_memory` really 320 or 160 entries?** Only 160 are used for synthesis overlap-add. The DLL allocates a larger state struct, and we sized it at 320 as a conservative match.

### COSINE_MOD_MATRIX
- **What are the last 220 entries?** Only the first 100 (10×10 matrix) are accessed by the cosine modulation phase. The remaining values are small (-8 to +11 range) and appear to be unrelated data or padding in the DLL's .rdata section. They may be:
  - A different table that was laid out adjacently
  - Initialization data for something else
  - Artifacts of compiler/linker padding

### Gain Huffman Tree
- **Why 23 nodes per section?** Each of the 13 sections (for up to 13 subband differentials) has exactly 23 nodes. The first 23 entries (section 0) are all zeros and appear unused. The symbol range is 0–23, which maps well to differential gain values, but the exact meaning of the 23-node size is unclear.
- **The tree has 300 entries (13 × 23 + 1 unused section).** With max 14 subbands, only 13 differentials are needed, so 13 sections suffice. The zeros at the beginning may be a sentinel/padding.

### Quantizer Details
- **QUANT_STEP_SIZE values are approximate Q15 reciprocals** of (QUANT_INV_STEP + 1). For example, step 0: QUANT_STEP_SIZE[0] = 2341 ≈ 32768/14. This is used for the iterated division in inverse_quantize via `mult(val, step_size)`. The precision implications of this approximation are unexplored.
- **Step 7 is "noise only"** — but what's the quantizer tree for step 7? CODEBOOK_TREE_6 (62 entries) is selected for step 6. Step 7 goes directly to noise fill without any codebook decode, so no tree is needed. But the QUANT_RECON_LEVELS[7] table exists with values [0, 8019] — is this ever used?

### Error Concealment
- **What happens on corrupted frames?** The current implementation detects bitstream exhaustion and fills remaining subbands with step-7 noise. The DLL also subtracts 1 from total_bits_remaining after error. But there may be more sophisticated error concealment that wasn't captured.

### DLL Exports and Calling Convention
- **The DLL exports** `a1800_dec_frame_init` and `a1800_dec_frame` (and encoder equivalents). The exact calling convention for the state struct pointer and its full layout beyond what we use is partially known:
  - The state struct is at least ~0x400 bytes
  - State offset 0x000: bitrate-derived parameters
  - State offset 0x1b0: synthesis memory (passed to synthesis_filter)
  - State offset 0x360: PRNG state (4 i16 values)
  - Full struct layout is not mapped

### Bit-Exactness
- **Has the decoder been validated against the DLL's output?** Not directly against the DLL. Round-trip testing (encode → decode) validates internal consistency: silence, DC, sine wave, and multi-frame tests pass. True bit-exact verification against the DLL still requires real .a18 test files and reference output.
- **The PRNG initialization** [1, 1, 1, 1] was read from the DLL's init function. If this is wrong, all noise-filled subbands will differ.

### Encoder Implementation Details
- `encode_gains` (0x100040b0): gain_index = `0xB + shift_count - 2*scale_param`, where shift_count comes from norm_s of subband energy. Backward smoothing clamps first gain to [-6, 24] and differentials to [-15, 24] before Huffman encoding.
- `prescale_subbands` (0x10003fe0): For each subband with gain > 0x27: `shift = shr(gain - 0x27, 1)`, then `sample = extract_l(L_shr(L_shr(L_add(L_shl(sample, 16), 0x8000), shift), 16))`. This is a right-shift with rounding, NOT multiplication by SCALE_FACTOR_BITS.
- `forward_quantize` (0x10004730): Computes `quant_scale = f(QUANT_SCALE_FACTOR[step], QUANT_SCALE_BY_GAIN[gain])`, then quantizes each sample as `level = (abs(sample) * quant_scale + QUANT_ROUNDING[step]) >> 13`, clamped to [0, QUANT_INV_STEP[step]]. Produces Huffman symbol indices via mixed-radix encoding. Uses FWD_CODEBOOK_CODES/WIDTHS tables (7 tables for steps 0–6).
- `analysis_filter` scale_param: Uses `L_mult(adj, 0x2573) → L_shr(,0x14) → norm_s`, NOT direct `norm_s(max_abs)`.

---

## 12. Known Differences Between Rust and DLL Implementations

The decoder is believed to be bit-exact with the DLL (not yet verified with real .a18 files). The encoder has a few remaining structural differences from the DLL but produces correct output. Round-trip encode→decode works for bitrates 4800–24000 bps with overall correlation ~0.60 and RMS ratio ~1.02 at 16 kbps. At 32000 bps the encoder's prescaling can cause all-zero quantization on the first frame.

### 12.1. Forward Quantize Bitpacking

**DLL**: `forward_quantize` at 0x10004730 packs Huffman codes and sign bits directly into 32-bit accumulators within the function, writing completed words to the output buffer as they fill. The function handles both quantization and bitstream generation in a single pass.

**Rust**: `encode_subframes` stores tuples of (width, code, num_signs, sign_bits) per subframe into `encoded_data`. A separate `write_bitstream` function then packs these into the output i16 words. Functionally equivalent but structurally different.

### 12.2. Frame Parameter Selection

**DLL** (`encode_subframes` at 0x100043e0): Starts at the midpoint (7 increments from scratch), encodes all subbands, then binary-searches:
- If total encoded bits < budget: undoes increments (frame_param--), re-encoding affected subbands
- If total encoded bits > budget: adds increments (frame_param++), re-encoding affected subbands

This uses actual Huffman-encoded bit counts for precise budget fitting.

**Rust** (`encoder::select_frame_param`): Iterates from frame_param=0 upward, computing the BIT_ALLOC_COST total at each step. Returns the minimum frame_param where estimated cost ≤ budget. Encodes only once with the final allocation.

**Impact**: The DLL's approach is more precise because it uses actual encoded bit counts. Ours uses the same cost model as the bit allocator (BIT_ALLOC_COST table), which is a good estimate but may differ from actual Huffman code lengths. This can occasionally cause the encoder to exceed the frame bit budget, which is handled gracefully by the BitstreamWriter stopping when the frame is full.

### 12.3. FWD_FILTERBANK_COEFF_5 (640 entries)

**DLL**: The FWD_FILTERBANK_COEFF_PTRS table has 6 entries, but the forward filterbank reconstruction loop only iterates 5 stages (4→0), using PTRS[0..4]. COEFF_5 (640 entries) is never accessed.

**Rust**: COEFF_5 is declared in tables.rs but not used by `filterbank::forward`.

**Status**: Either COEFF_5 is dead data, or it's used by a frame size other than 320 that we haven't encountered.

### 12.4. Previously Fixed Differences

The following differences existed in earlier versions but have been corrected to match the DLL:

- **Forward filterbank phase ordering** (fixed in c4f255a): Now uses butterfly→cosine→reconstruct order matching the DLL at 0x10002280. All butterfly stages use 32-bit precision with sequential read / front-back write pattern.
- **Analysis filter windowed overlap** (fixed in c4f255a): Second loop now uses `ANALYSIS_WINDOW[n-1-k]` and `negate(ANALYSIS_WINDOW[k])` matching the DLL's pointer arithmetic at 0x10004ba0.
- **Analysis filter memory copy** (fixed in c4f255a): Now copies all 320 PCM samples into memory, not just the second half.
- **Forward quantize algorithm** (fixed in c4f255a): Now uses the DLL's formula-based quantization: `level = (abs(sample) * quant_scale + QUANT_ROUNDING[step]) >> 13` with `quant_scale` derived from `QUANT_SCALE_FACTOR[step]` and `QUANT_SCALE_BY_GAIN[gain]`.

---

## 13. Rust Implementation Structure

```
src/
├── main.rs          CLI: a1800_codec decode/encode
├── lib.rs           Public API: A1800Decoder + A1800Encoder
├── fixedpoint.rs    ITU-T G.729-style basic operations (25 functions)
├── bitstream.rs     MSB-first bit reader/writer from 16-bit LE words
├── decoder.rs       Frame decoder: gains, bit allocation, subframe decode
├── encoder.rs       Frame encoder: gains, bit allocation, forward quantize, bitstream write
├── filterbank.rs    5-stage butterfly + cosine mod + reconstruction (inverse + forward)
├── analysis.rs      Analysis filter: windowed overlap + forward filterbank + scale_param
├── synthesis.rs     Inverse filterbank + scaling + overlap-add
├── tables.rs        All constant tables from the DLL (decoder + encoder)
└── wav.rs           Mono 16-bit PCM WAV reader + writer
```

52 tests cover: fixed-point ops (9), bitstream reader/writer (7), decoder core (6), encoder core + round-trip (20), filterbank (5), synthesis (2), analysis (1), WAV reader/writer (2).

---

## 14. CLI Usage

```
a1800_codec decode <input.a18> <output.wav> [--sample-rate N]
a1800_codec encode <input.wav> <output.a18> [--bitrate N]
```

Decode: reads bitrate from the .a18 header, default sample rate 16000 Hz.
Encode: default bitrate 16000 bps, supports 4800–24000 (32000 has known limitation).