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.
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.
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.
| 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).
Per frame, the decode pipeline is:
Bitstream → Gain Decode → Bit Allocation → Subframe Decode → Inverse Filterbank → Synthesis
- Read 5-bit initial gain index →
initial_gain = index - 7 - 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)
- Cumulative gains:
gain[i+1] = gain[i] + differential[i] - 12 - 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
- Final scale factors:
scale_factor[i] = SCALE_FACTOR_BITS[gain[i] + sp*2 + 24]
A 4-bit value consumed from the bitstream after gains. Used to fine-tune bit allocation via increment_allocation_bins.
Three-step process:
-
Budget adjustment: If remaining bits > 320, compress excess:
budget = ((remaining - 320) * 5) >> 3 + 320 -
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
-
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*stepmetric) - Over budget → increase best subband (largest metric, scanning from top subband down)
- Records operations in a swap log
-
Apply frame parameter: The 4-bit value indexes into the swap log to replay N increment operations.
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-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].
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).
Windowed overlap-add producing 320 PCM samples from inverse filterbank output + 160-sample memory from previous frame.
- Call inverse_filterbank →
filtered[0..319] - Apply scale_param: if > 0,
shr(sample, scale_param); if < 0,shl(sample, -scale_param) - 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)) - 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)) - 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.
Per frame, the encode pipeline mirrors the decode pipeline in reverse:
PCM → Analysis Filter → Gain Encode → Bit Allocation → Subframe Encode → Bitstream Pack
Converts 320 PCM samples to 320 subband samples + returns scale_param:
- 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]))
- First 160 outputs: symmetric window applied to memory buffer
- Copy all 320 pcm_input samples into memory for next frame's overlap
- 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
- Apply scaling: if sp > 0 → shl; if sp < 0 → shr
- Forward filterbank to produce subbands
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.
- For each subband, compute energy:
sum(sample² for 20 samples)viaL_mac0 - Convert to log-scale gain index (normalize via leading-zero count)
- Clamp first gain to [-6, 24], subsequent differentials to [-15, 24]
- Huffman-encode differentials using
GAIN_HUFFMAN_BIT_WIDTHSandGAIN_HUFFMAN_CODEStables - Returns total bits consumed by gain encoding
encode_gains→ gain indices + Huffman codescompute_bit_alloc_for_frame→ per-subband allocation (same function as decoder)prescale_subbands→ normalize subband samples by gain (right-shift proportional to gain)encode_subframes→ quantize each subband viaforward_quantize, pack into coded datawrite_bitstream→ assemble gain codes + 4-bit frame parameter + coded subbands into output words
Packs the following into 16-bit LE output words:
- Per-subband gain Huffman codes (variable width)
- 4-bit frame parameter (from optimization swap count)
- 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)
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)
All tables were extracted from the DLL's .rdata section via Ghidra memory inspection.
| 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 |
| 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 |
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
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.
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 | ab2 with saturation for 0x40000000 |
| L_mac | (i32, i16, i16) → i32 | acc + ab2 |
| 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) |
| 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 |
| 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 |
| 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 |
| 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) |
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 zerosfilterbank_memory: [i16; 640]— allocated but purpose unclear (filterbank is stateless in our analysis; see unknowns)
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.
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.
- 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.
- 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_sizeparameter and special-cases 320 with a final ×2 scaling. Other sizes may exist but are untested.
- 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.
- Why the budget cap at 320? The budget adjustment formula
((excess - 320) * 5) >> 3 + 320limits 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.
- 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_memoryreally 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.
- 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
- 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.
- 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?
- 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.
- The DLL exports
a1800_dec_frame_initanda1800_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
- 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.
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), thensample = 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): Computesquant_scale = f(QUANT_SCALE_FACTOR[step], QUANT_SCALE_BY_GAIN[gain]), then quantizes each sample aslevel = (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_filterscale_param: UsesL_mult(adj, 0x2573) → L_shr(,0x14) → norm_s, NOT directnorm_s(max_abs).
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.
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.
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.
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.
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]andnegate(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]) >> 13withquant_scalederived fromQUANT_SCALE_FACTOR[step]andQUANT_SCALE_BY_GAIN[gain].
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).
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).