# Radium Image Format

## Summary

Static Ghidra and file-structure analysis now identifies the main sound payload area in `/games/pokemon_pro/image.bin`.

Confirmed findings:

- `/games/pokemon_pro/image.bin` is mmap-backed by the native game loader, not a standard archive.
- The first `13` little-endian qwords form the image header copied by `FUN_008733e0`.
- Header qwords `0` through `9` are section offsets used by `FUN_008736c0`.
- Header qword `12` low32 gives the section 8 row count. For the main game image this is `0x9bb` (`2491`) rows.
- Section 8 starts at file offset `0x44d5396f`; each row is `24` bytes: `q0`, `q1`, `q2`.
- `q0` is the data offset for a headerless PCM chunk.
- `q1` contains the masked lookup key and packed width/channel flags used by `FUN_007a46e0`.
- `q2` low32 is the sample count used by the loader.
- Section 3 is a pointer table with `1477` entries. The pointed records begin with `uint32 id`, `uint32 flags`, `uint16 width`, and `uint16 height`; `1449` records currently render as raw indexed bitmap previews.
- Section 5 is a dense command-stream table with `2531` local object slots and `5` variants per slot.
- The loader's byte-length formula for opcode `0x0b` references is `derived_multiplier * (q2 & 0xffffffff) * 2`.
- Applying that formula to all section 8 rows produces `2491` bounded PCM candidate chunks. For `2490` rows, the computed byte length exactly equals the distance to the next row's `q0` offset. The last row has no next-row delta for comparison.
- The section 8 checksum is CRC32 over the aligned table bytes. The stored value `0x3f401e79` matches the calculated value.

Conclusion: the missing sounds are not absent and are not plain Ogg/WAV files. They are stored as headerless 16-bit PCM ranges inside `/games/pokemon_pro/image.bin`, indexed by the Radium section 8 table. The section 5 command-stream scan now resolves every section 8 PCM chunk to at least one opcode `0x0b` reference. A small native `GodotSpikeBridge` registration table maps `25` object IDs to confirmed `SE_*` names. A Pokedex voice alignment now maps `1137` Radium objects (`409` through `1545`) to Pokemon description/name/category clips using `assets/godot_raw/data/pokemon_data.txt` and ASR evidence. The aligned Pokedex stream is normally description, name, category, with six duplicate description variants: Magmar, Flareon, Snorlax, Numel, Magmortar, and Talonflame. Section 3 also contains small bitmap-like records, and `1449` raw indexed records are now exported as PNG previews. The remaining format work is mapping the other command-stream/object IDs to higher-level script/gameplay names and playback context, plus finishing the compact section 3 subtype and section 4 semantics.

The same section-8 PCM table structure is now also confirmed in `/games/pokemon_pro/spike3/spike_menu/image.bin`. That smaller control image has `54` section 8 rows, a matching section-8 CRC32, `300` resolved section 5 opcode `0x0b` references, and `54` bounded PCM candidates exported as WAVs under `analysis/radium-spike-menu-image/sounds/`.

## Reproducible Parser

Generated parser:

```sh
python3 rootfs-triage-wiki/scripts/parse_radium_image.py
```

Generated outputs:

- `analysis/radium-image/header.json`
- `analysis/radium-image/section4_table.tsv`
- `analysis/radium-image/section3_bitmap_manifest.tsv`
- `analysis/radium-image/section3-bitmaps/*.png`
- `analysis/radium-image/section8_entries.tsv`
- `analysis/radium-image/pcm_candidates.tsv`
- `analysis/radium-image/command_sound_refs.tsv`
- `analysis/radium-image/command_stream_summary.tsv`
- `analysis/radium-image/command_opcode_rows.tsv`
- `analysis/radium-image/command_opcode_histogram.tsv`
- `analysis/radium-image/opcode_handler_table.tsv`
- `analysis/radium-image/native_sound_id_map.tsv`
- `analysis/radium-image/native_named_sound_manifest.tsv`
- `analysis/radium-image/native_sound_test_label_manifest.tsv`
- `analysis/radium-image/catalog_offset_candidate_labels.tsv`
- `analysis/radium-image/asr_draft_sound_labels.tsv`
- `analysis/radium-image/asr_review_sound_label_candidates.tsv`
- `analysis/radium-image/pokedex_voice_label_manifest.tsv`
- `analysis/radium-image/sound_export_manifest.tsv`
- `analysis/radium-image/sound_export_manifest.json`
- `analysis/radium-image/sounds/*.wav`
- `analysis/radium-image/sounds-native-named/*.wav`
- `analysis/radium-image/sounds-native-sound-test/*.wav`
- `analysis/radium-image/sounds-pokedex-voice/*.wav`
- `analysis/radium-image/sounds-best-named/*.wav`
- `analysis/all-sound-best-name-manifest.tsv`

The same parser was also run against the SPIKE menu image. Its companion binary uses the same opcode-length sequence at file/Ghidra offset `0x410ce8`, so the parser is run with an image-specific opcode table offset:

```sh
python3 rootfs-triage-wiki/scripts/parse_radium_image.py --image games/pokemon_pro/spike3/spike_menu/image.bin --game-binary games/pokemon_pro/spike3/spike_menu/game --out-dir analysis/radium-spike-menu-image --sound-dir analysis/radium-spike-menu-image/sounds --opcode-size-table-offset 0x410ce8 --object-table-count 0 --write-all-wavs
```

Generated SPIKE menu control outputs:

- `analysis/radium-spike-menu-image/header.json`
- `analysis/radium-spike-menu-image/section4_table.tsv`
- `analysis/radium-spike-menu-image/section8_entries.tsv`
- `analysis/radium-spike-menu-image/pcm_candidates.tsv`
- `analysis/radium-spike-menu-image/command_sound_refs.tsv`
- `analysis/radium-spike-menu-image/command_stream_summary.tsv`
- `analysis/radium-spike-menu-image/command_opcode_rows.tsv`
- `analysis/radium-spike-menu-image/command_opcode_histogram.tsv`
- `analysis/radium-spike-menu-image/opcode_handler_table.tsv`
- `analysis/radium-spike-menu-image/sound_export_manifest.tsv`
- `analysis/radium-spike-menu-image/sound_export_manifest.json`
- `analysis/radium-spike-menu-image/sounds/*.wav`

Preview WAV export, capped to avoid duplicating the full 1.15 GB image:

```sh
python3 rootfs-triage-wiki/scripts/parse_radium_image.py --write-wav-previews --max-wavs 20
```

Preview outputs:

- `analysis/radium-image/wav-preview-manifest.json`
- `analysis/radium-image/wav-preview/section8_0000_1ch_44100hz.wav`
- `analysis/radium-image/wav-preview/section8_0001_1ch_44100hz.wav`
- Additional preview WAVs through `section8_0019_*`.

The parser is static and does not execute target AArch64 binaries.

Opcode handler/length table dump:

```sh
python3 rootfs-triage-wiki/scripts/dump_radium_opcode_tables.py
```

This writes `analysis/radium-image/opcode_handler_table.tsv` and `analysis/radium-spike-menu-image/opcode_handler_table.tsv`. Each output row records the 16-byte native table entry for one opcode: stored handler address, payload length, observed command counts, and the most common payloads from the generic stream decode.

Section 3 bitmap previews:

```sh
python3 rootfs-triage-wiki/scripts/export_radium_section3_bitmaps.py
```

This writes:

- `analysis/radium-image/section3_bitmap_manifest.tsv`
- `analysis/radium-image/section3-bitmaps/*.png`
- Wiki previews in `radium-section3-bitmaps.md`

The section 3 exporter is static and intentionally conservative: it emits PNG previews only when the record payload is large enough for raw `width * height` indexed pixels, and records compact payloads as undecoded.

Confirmed native-name aliases:

```sh
python3 rootfs-triage-wiki/scripts/extract_godot_gdc_strings.py
python3 rootfs-triage-wiki/scripts/extract_native_sound_registrations.py
python3 rootfs-triage-wiki/scripts/build_native_named_sound_aliases.py
python3 rootfs-triage-wiki/scripts/extract_native_sound_test_labels.py
python3 rootfs-triage-wiki/scripts/build_catalog_offset_candidate_labels.py
```

These write:

- `analysis/godot-introspection/gdc_script_summary.tsv`
- `analysis/godot-introspection/gdc_strings.tsv`
- `analysis/godot-introspection/gdc_sound_refs.tsv`
- `analysis/radium-image/native_sound_id_map.tsv`
- `analysis/radium-image/native_named_sound_manifest.tsv`
- `analysis/radium-image/native_sound_test_label_manifest.tsv`
- `analysis/radium-image/catalog_offset_candidate_labels.tsv`
- `analysis/radium-image/sounds-native-named/*.wav`
- `analysis/radium-image/sounds-native-sound-test/*.wav`

Full WAV export:

```sh
python3 rootfs-triage-wiki/scripts/parse_radium_image.py --write-all-wavs
```

This writes one RIFF/WAV file per section 8 row under `analysis/radium-image/sounds/`. Current output count is `2491` WAV files. The deterministic filename format is:

```text
objNNNN_vV_cmdC_sSSSS_channels_rate.wav
```

The `objNNNN` prefix is the best available static name from the resolved Radium local object ID. `sSSSS` preserves the section 8 chunk index so every filename stays unique and traceable even if higher-level names are recovered later.

Complete best-name aliases:

```sh
python3 rootfs-triage-wiki/scripts/build_pokedex_voice_aliases.py
python3 rootfs-triage-wiki/scripts/build_asr_catalog_sound_labels.py
python3 rootfs-triage-wiki/scripts/build_best_named_sound_aliases.py
```

This creates one alias row per exported sound across the main game and SPIKE menu images. The source priority is confirmed native name, Pokedex voice alignment, catalog-matched ASR constant, accepted draft ASR transcript, catalog-offset candidate, low-confidence ASR review candidate, exact duplicate hash match against a stronger main-image name, SPIKE-menu hash match against a stronger main-image name, then structural object/chunk reference. Current output is `2545` rows in `analysis/all-sound-best-name-manifest.tsv`: `2491` main-game sounds and `54` SPIKE menu sounds. Current name-source counts are `1137` Pokedex voice alignments, `220` catalog-matched ASR aliases, `583` raw ASR draft aliases, `196` catalog-offset candidate aliases, `34` low-confidence ASR review candidate aliases, `295` main structural object fallbacks, `25` confirmed native names, `1` main-image duplicate hash match, `4` SPIKE-menu hash matches to stronger main-image names, and `50` SPIKE-menu structural fallbacks. Catalog-matched ASR aliases are inferred only when an accepted transcript exactly matches a known `SE_PK_VO_*` Godot catalog phrase or a narrow documented ASR error variant; the current overlay covers `90` distinct catalog constants. Catalog-offset aliases are marked `candidate_unpromoted`, are lower priority than direct content evidence, and do not claim a general Godot catalog-to-Radium object map. Low-confidence ASR review candidates are content-derived only and are used only when no stronger semantic label exists. The main-image duplicate is object `192` / section 8 `903`, which is SHA-256 identical to confirmed-native object `213` / section 8 `818` (`SE_PK_FX_ATTACK_TYPE_WATER`). The alias WAVs are under `analysis/radium-image/sounds-best-named/` and `analysis/radium-spike-menu-image/sounds-best-named/`.

## Header Evidence

Main image path: `/games/pokemon_pro/image.bin`

File size: `0x44d62307` (`1154884359`) bytes.

Header qwords from `analysis/radium-image/header.json`:

| Index | Value | Meaning |
| ---: | ---: | --- |
| `0` | `0x00000000000000b0` | Section 0 offset |
| `1` | `0x000000000001be18` | Section 1 offset |
| `2` | `0x000000000001bdf8` | Section 2 offset |
| `3` | `0x0000000000018fd0` | Section 3 offset |
| `4` | `0x0000000000018c70` | Section 4 offset |
| `5` | `0x00000000000000f8` | Section 5 offset |
| `6` | `0x00000000000000f8` | Section 6 offset |
| `7` | `0x0000000044d62303` | Near-EOF marker/section offset |
| `8` | `0x0000000044d5396f` | Section 8 table offset |
| `9` | `0x0000000000018c70` | Section 9 offset |
| `10` | `0x000005c50000001b` | Count pair: low32 `27`, high32 `1477` |
| `11` | `0x000009e300000000` | Count pair: high32 `2531` |
| `12` | `0x00000000000009bb` | Section 8 row count: `2491` |

Ghidra evidence:

- `FUN_008733e0(uint image_index)` opens `image.bin` or `image-sc%02d.bin`, mmaps it read-only, and copies the first `13` qwords into runtime image metadata.
- `FUN_008736c0(image_index, section_id)` returns `mmap_base + header_qword[section_id]` for section IDs `0` through `9`.
- `FUN_00873290(image_index, count_id)` returns count fields from the copied header. Count ID `4` returns `2491` for the main image.

## Section 3 Bitmap Records

Ghidra xrefs to `FUN_008736c0` identify section 3 as a count-indexed pointer table:

- `FUN_0080aca0(index)` initializes `DAT_033e2658 = FUN_008736c0(0, 3)` and `DAT_033e2660 = FUN_00873290(0, 1)`, then returns `section3 + index * 8`.
- `FUN_0080d1b0(index)` and `FUN_0080d250(index)` fetch the qword pointer from section 3, pass it through `FUN_00873360(0, pointer)`, and read signed 16-bit fields at pointed-record offsets `+8` and `+10`.
- Count ID `1` is header qword 10 high32, `1477`, matching the number of section 3 qword pointers.

Static record observations from `/games/pokemon_pro/image.bin`:

| Field | Evidence |
| --- | --- |
| Pointer table offset | Section 3 at `0x18fd0` |
| Pointer count | `1477` |
| Record header | `uint32 record_id`, `uint32 flags`, `uint16 width`, `uint16 height` |
| Common first dimensions | `17 x 23`, `5 x 7`, `7 x 7`, `128 x 32` |
| Current decoder output | `1449` `raw_indexed_preview` PNGs and `28` `undecoded_compact_payload` rows |

`rootfs-triage-wiki/scripts/export_radium_section3_bitmaps.py` writes `analysis/radium-image/section3_bitmap_manifest.tsv` and grayscale/alpha PNG previews under `analysis/radium-image/section3-bitmaps/`. Pixel values are rendered as grayscale for preview; `0xff` is rendered transparent. This is a visual reconstruction aid, not a confirmed final palette. The `28` compact payload rows, mostly `128 x 32` records with payloads smaller than `width * height`, are deliberately left undecoded until their compression/packing rule is recovered.

The wiki exposes these previews in `radium-section3-bitmaps.md`.

## Section 8 Chunk Table

Ghidra evidence from `FUN_007a46e0(ulong image_id)`:

- Calls `FUN_00873290(image_id, 4)` to get section 8 row count.
- Calls `FUN_008736c0(image_id, 8)` to get the section 8 table pointer.
- Copies `(row_count * 0x18 + 0xf) & ~0xf` bytes and validates CRC32 with `FUN_00a2e1f0`.
- Iterates row triples:
  - `q0 = row[0]`
  - `q1 = row[1]`
  - `q2 = row[2]`
- Stores `q0`, `q2` low32, and packed flags in a runtime lookup map keyed by `image_id << 0x22 | (q1 & 0xfffc0003ffffffff)`.
- Later, opcode `0x0b` command streams look up this map and compute byte length as:

```c
byte_length = derived_multiplier * q2_low32 * 2;
```

The parser implements the same flag extraction seen in `FUN_007a46e0`.

Section 8 validation results:

| Metric | Value |
| --- | ---: |
| Section 8 offset | `0x44d5396f` |
| Row count | `2491` |
| Row size | `24` bytes |
| Raw table bytes | `59784` |
| Aligned CRC bytes | `59792` |
| Stored CRC32 | `0x3f401e79` |
| Calculated CRC32 | `0x3f401e79` |
| Rows with bounded nonzero PCM length | `2491` |
| Rows where computed PCM length equals next `q0` delta | `2490` |

SPIKE menu image section 8 validation results:

| Metric | Value |
| --- | ---: |
| Image path | `/games/pokemon_pro/spike3/spike_menu/image.bin` |
| File size | `0xe443a1` (`14959521`) bytes |
| Section 8 offset | `0xe43e89` |
| Row count | `54` |
| Row size | `24` bytes |
| Raw/aligned table bytes | `1296` |
| Stored CRC32 | `0xb8fe11f9` |
| Calculated CRC32 | `0xb8fe11f9` |
| Rows with bounded nonzero PCM length | `54` |
| Rows where computed PCM length equals next `q0` delta | `53` |
| Resolved opcode `0x0b` references | `300` |
| Unique section 8 chunks referenced | `54` |
| Unique local object IDs with references | `59` |
| Exported WAV count | `54` |
| Channel counts | `16` mono, `38` stereo |

First decoded rows from `analysis/radium-image/section8_entries.tsv`:

| Index | `q0` | Next delta | Multiplier | Sample count | PCM bytes | First bytes |
| ---: | ---: | ---: | ---: | ---: | ---: | --- |
| `0` | `0x4f71e` | `278896` | `1` | `139448` | `278896` | `fbffeffff3ffefff...` |
| `1` | `0x9388e` | `91134` | `1` | `45567` | `91134` | `0000000000000000...` |
| `2` | `0xa9c8c` | `139316` | `1` | `69658` | `139316` | `ffff0300fdff0300...` |
| `3` | `0xcbcc0` | `493628` | `2` | `123407` | `493628` | `fdff01000300fdff...` |
| `4` | `0x1446fc` | `541500` | `2` | `135375` | `541500` | `01000100ffffffff...` |

The repeated small signed 16-bit values in the first bytes are consistent with little-endian PCM sample data.

## Command Stream References

The parser now scans the full section 5 command-stream table used by `FUN_007a46e0`.

Static game-binary tables:

- Object table base: Ghidra/raw address `0x032521c0`.
- Object table count: `0x3f1` entries.
- Object table stride: `0x20` bytes.
- Each entry's first qword points to a zero-terminated `uint32_t` object-ID list in `/games/pokemon_pro/game`.
- The flag byte used by the preload path is at entry offset `+0x0d`.
- Opcode handler/size table base: Ghidra/raw address `0x03079e70`, with `0x13` entries and a `0x10`-byte stride.
- Each opcode table entry is `qword handler_address`, then `qword payload_length`. `FUN_007a46e0` indexes the second qword directly through `DAT_03179e78` to skip non-sound opcodes in the preload scan; the runtime interpreter handlers live in the first qword.
- The SPIKE menu companion binary has the same 16-byte table shape at Ghidra/raw address `0x410ce0`; its payload-length qword sequence begins at `0x410ce8`, matching the parser argument used for menu extraction.

Static image tables:

- Section 2 byte at `section2 + 7` is `5`, used here as the variant slot count.
- Section 5 begins at offset `0xf8` and maps `(object_local_id * variant_count + variant)` to command-stream offsets.
- Section 5 ends at section 4 offset `0x18c70`; `(0x18c70 - 0xf8) / (5 * 8) = 2531` local object slots.
- Opcode `0x0b` records are `12` bytes and carry a section 8 lookup key at command offset `+4`.
- The object table is still used for cross-checking and metadata, but it is not a complete enumerator for section 5. A previous object-table-only scan missed `530` opcode `0x0b` references and `62` section 8 chunks, including local object `184` (`SOUND: SUPER SKILL SHOT`).

Command-reference extraction results from `analysis/radium-image/header.json`:

| Metric | Value |
| --- | ---: |
| Resolved opcode `0x0b` references | `12710` |
| Unique local object IDs with references | `2521` |
| Unique section 8 chunks referenced | `2491` |

Generic section 5 command-stream decode results:

| Image | Streams | Unique stream offsets | Opcode rows | Unique opcodes | Streams with sound refs | Malformed streams |
| --- | ---: | ---: | ---: | --- | ---: | ---: |
| Main game | `12655` | `2531` | `108765` | `0x01,0x02,0x03,0x04,0x05,0x07,0x09,0x0a,0x0b,0x0e,0x10,0x11,0x12` | `12605` | `0` |
| SPIKE menu | `340` | `68` | `2065` | `0x02,0x04,0x05,0x09,0x0a,0x0b,0x0e,0x10,0x11,0x12` | `295` | `0` |

All decoded streams terminate with opcode `0x00`. The largest decoded stream in both images is `108` bytes / `18` opcodes. Detailed rows are in `command_stream_summary.tsv`, `command_opcode_rows.tsv`, `command_opcode_histogram.tsv`, and `command_opcode_payload_summary.tsv` under each analysis directory.

Payload-summary observations:

| Image | Unique stream patterns | Most common pattern | Count |
| --- | ---: | --- | ---: |
| Main game | `14` | `0x05,0x02,0x09,0x09,0x0b,0x10,0x09,0x09,0x11` | `10345` |
| SPIKE menu | `6` | `0x05,0x02,0x0b,0x11` | `220` |

The SPIKE menu pattern set is a subset of the main image patterns. Main-only control opcodes are `0x01`, `0x03`, and `0x07`; the menu still uses the shared object setup (`0x05`), parameter selector (`0x02`), envelope/parameter records (`0x09`, `0x10`, `0x11`), sound reference (`0x0b`), and the less-common `0x04`, `0x0a`, `0x0e`, `0x12` records.

Opcode payload shapes from `command_opcode_payload_summary.tsv`:

| Opcode | Main count | Menu count | Payload bytes | Static interpretation status |
| --- | ---: | ---: | ---: | --- |
| `0x05` | `12655` | `340` | `6` | Object/clip setup record; first byte clusters by stream family, middle fields vary per object. |
| `0x02` | `12600` | `290` | `2` | Selector/state byte pair; often follows `0x05`. |
| `0x09` | `44755` | `150` | `15` | Parameter/envelope record; only `17` unique main payloads and `7` menu payloads. |
| `0x0b` | `12710` | `300` | `11` | Confirmed sound-reference command; byte 0 is command channel and bytes 3-10 carry the section 8 lookup key. |
| `0x10` | `12100` | `145` | `4` | Little-endian scalar, often duration-like; common value `1000` appears in both images. |
| `0x11` | `12645` | `230` | `1` | Constant payload `01`, likely end/play trigger paired after sound setup. |
| `0x04` | `520` | `520` | `2` | Repeated small selector rows, always second byte `7`. |
| `0x0a` | `50` | `50` | `1` | Small selector, values `1` or `2`. |
| `0x0e` | `20` | `20` | `4` | Scalar values `1` or `2`. |
| `0x12` | `20` | `20` | `8` | Two payloads: `ffffffff401f0000` and `feffffff401f0000`. |

Opcode table details from `opcode_handler_table.tsv`:

| Opcode | Main payload length | Main handler | Main count | Menu payload length | Menu handler | Menu count | Note |
| --- | ---: | --- | ---: | ---: | --- | ---: | --- |
| `0x02` | `2` | `0x69dd10` | `12600` | `2` | `0xec1c0` | `290` | Common selector/state byte pair. |
| `0x05` | `6` | `0x69de10` | `12655` | `6` | `0xec2c0` | `340` | First command in every decoded stream. |
| `0x09` | `15` | `0x69dea0` | `44755` | `15` | `0xec350` | `150` | Small fixed payload vocabulary. |
| `0x0b` | `11` | `0x6a3140` | `12710` | `11` | `0xf1670` | `300` | Confirmed sound-reference payload; total command length is `12` including opcode byte. |
| `0x10` | `4` | `0x69e0a0` | `12100` | `4` | `0xec550` | `145` | Little-endian scalar values, often near paired sound records. |
| `0x11` | `1` | `0x69e100` | `12645` | `1` | `0xec5b0` | `230` | Constant payload `01`. |
| `0x12` | `8` | `0x69e160` | `20` | `8` | `0xec610` | `20` | Rare branch/loop-looking payload pair in both images. |

Ghidra evidence from `FUN_007a46e0` confirms preload semantics for `0x0b`: it reads `pbVar15[1]` as the command channel, masks `*(ulong *)(pbVar15 + 4)` with `0xfffc0003ffffffff`, looks up the section 8 entry, and advances by `0x0c` bytes. For other opcodes in the preload path, the function advances by the payload-length qword at `DAT_03179e78 + opcode * 0x10`; the handler-address side of that table now gives concrete native addresses for follow-up decompilation, but it does not by itself name the command semantics.

`objdump -d --start-address=0x69e1d0 --stop-address=0x69e250 games/pokemon_pro/game` cross-checks the table alignment: the runtime handler near `0x69e1e0` loads table base `0x3079e70`, then reads the payload length from entry offset `+8`. This is why `parse_radium_image.py` keeps its existing `0x3079e78` length-table offset while `dump_radium_opcode_tables.py` records the full entry base at `0x3079e70`.

Caveat: section 5 has five variant slots. Many variants currently point to the same command stream, so `command_sound_refs.tsv` intentionally preserves the per-variant rows even when the stream offset repeats. `530` references do not have object-table metadata; those rows still have section-5 local object IDs and blank `object_table_index` / `object_table_flag` fields.

First resolved rows from `analysis/radium-image/command_sound_refs.tsv`:

| Object ID | Variant | Stream offset | Command offset | Section 8 index | `q0` | PCM bytes |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| `9` | `0` | `0x44d1e0d0` | `0x44d1e0da` | `2084` | `0x3a76c5a2` | `36612` |
| `9` | `1` | `0x44d1e0d0` | `0x44d1e0da` | `2084` | `0x3a76c5a2` | `36612` |
| `10` | `0` | `0x44d1e0e9` | `0x44d1e0f3` | `1679` | `0x2e5d6e12` | `56196` |
| `11` | `0` | `0x44d1e102` | `0x44d1e10c` | `543` | `0x10575c9c` | `52740` |

This confirms that the command streams directly reference section 8 PCM chunks by packed key. The current output still uses numeric object IDs; mapping those IDs to script names, call sites, or gameplay events remains open.

## Native Sound-Test-Adjacent Labels

The native binary contains a small label table near the sound-test UI data. A previous pass started at raw/Ghidra offset `0x0325d940` and decoded each row as a localized label-list pointer followed by a numeric field. Wider table inspection shows nearby speaker-test rows at `0x0325d900` through `0x0325d930`, followed by rows at `0x0325d940` through `0x0325d970` that look like sound-test labels.

The numeric field is not confirmed to be a direct Radium local sound ID. Two independent cross-checks disprove direct use for every row: object `65` is ASR-labeled as "Cabinet speaker" while the candidate table row says `MUSIC: MAINPLAY`, and object `477` is the Pokedex `0030` Nidorina category clip ("Poison Pin") while the candidate row says `SPEECH: 'SQUIRTLE'`.

Current candidate rows retained for audit:

| Label | Numeric field | Source note |
| --- | ---: | --- |
| `MUSIC: MAINPLAY` | `65` | Sound-test-adjacent candidate; not promoted to best-name export. |
| `MUSIC: TEAM ROCKET` | `81` | Sound-test-adjacent candidate; not promoted to best-name export. |
| `SOUND: SUPER SKILL SHOT` | `184` | Sound-test-adjacent candidate; not promoted to best-name export. |
| `SPEECH: 'SQUIRTLE'` | `477` | Conflicts with confirmed Pokedex voice alignment. |

`rootfs-triage-wiki/scripts/extract_native_sound_test_labels.py` preserves these rows and preview aliases under `analysis/radium-image/sounds-native-sound-test/` for audit only. `build_best_named_sound_aliases.py` no longer promotes the candidate labels into the best-name manifest.

## Draft ASR Sound Labels

The full WAV export is also suitable for local speech-to-text triage. `rootfs-triage-wiki/scripts/transcribe_sound_draft_labels.py` runs local Whisper against WAV candidates and writes accepted draft transcripts to `analysis/radium-image/asr_draft_sound_labels.tsv`.

Current ASR pass:

| Metric | Value |
| --- | ---: |
| Rows transcribed | `2486` |
| Accepted draft labels | `1705` |
| Model | `tiny.en` |
| Duration coverage | `0.25s` and longer |
| Channels | `1` or `2` |
| Decode temperature | `0.0` |

Reproduction commands used for the current cumulative pass:

```sh
/opt/homebrew/Cellar/openai-whisper/20250625_4/libexec/bin/python rootfs-triage-wiki/scripts/transcribe_sound_draft_labels.py --model tiny.en --model-dir /Users/jordan/.cache/whisper --min-duration 0.25 --max-duration 15.0 --channels 1,2 --resume --temperature 0.0
/opt/homebrew/Cellar/openai-whisper/20250625_4/libexec/bin/python rootfs-triage-wiki/scripts/transcribe_sound_draft_labels.py --model tiny.en --model-dir /Users/jordan/.cache/whisper --min-duration 15.0 --max-duration 9999 --channels 1,2 --resume --temperature 0.0
```

The second long-clip pass added `39` rows and `0` accepted labels under the current confidence filters. Five sub-`0.25s` clips remain outside the ASR pass.

These transcripts are content-derived labels only. They are useful for preview search and for prioritizing voice-like clips, but they are not native symbols and may contain recognition errors. The wiki exposes accepted draft rows in `sound-asr-draft-labels.md` and adds draft titles to the full sound galleries when no confirmed native label exists.

`rootfs-triage-wiki/scripts/build_asr_review_sound_labels.py` also applies conservative review filters to rejected ASR rows. It writes `analysis/radium-image/asr_review_sound_label_candidates.tsv`, currently with `169` low-confidence review candidates. `build_best_named_sound_aliases.py` uses only `34` of those rows as `name_source=asr_review_candidate`, because stronger confirmed, aligned, catalog-ASR, accepted-ASR, and catalog-offset labels take priority. These aliases are marked `confidence=low_confidence_asr` and remain content-derived review aids, not confirmed symbols.

## Pokedex Voice Alignment

`rootfs-triage-wiki/scripts/build_pokedex_voice_aliases.py` aligns Radium local objects `409` through `1545` against `assets/godot_raw/data/pokemon_data.txt`.

Static evidence:

- The aligned range contains `1137` exported WAVs.
- The Pokemon data table has `377` `in_game=TRUE` non-`Blank` rows; the normal triplet stream would be `1131` clips.
- The six extra clips are duplicate description variants for Magmar (`obj0683`), Flareon (`obj0708`), Snorlax (`obj0715`), Numel (`obj0941`), Magmortar (`obj1095`), and Talonflame (`obj1270`).
- The alignment uses the local ASR transcript as evidence, but the canonical names come from `pokemon_data.txt`: `Pokedex NNNN Pokemon role`.
- Output aliases are under `analysis/radium-image/sounds-pokedex-voice/` and previewed in `sound-pokedex-voice.md`.

Example confirmed alignment rows:

| Object | Section 8 | Canonical label | Evidence |
| ---: | ---: | --- | --- |
| `409` | `723` | `Pokedex 0001 Bulbasaur description` | ASR matches the Bulbasaur description from `pokemon_data.txt`. |
| `410` | `1553` | `Pokedex 0001 Bulbasaur name` | ASR transcript is `Bulbasaur!`. |
| `411` | `598` | `Pokedex 0001 Bulbasaur Seed category` | ASR transcript is `The Seed Pokemon`. |
| `683` | `789` | `Pokedex 0126 Magmar description alt 2` | Duplicate Fahrenheit wording variant aligned after the first Magmar description. |
| `1545` | `1524` | `Pokedex 1019 Hydrapple Apple Hydra category` | End of the aligned Pokedex stream. |

## Confirmed Native Sound Names

Ghidra and static disassembly identify a native Godot bridge registration table in `/games/pokemon_pro/game`:

- `strings -a /games/pokemon_pro/game` finds `GodotSpikeBridge::register_sound(...)` and 25 `SE_PK_*` cue names.
- Ghidra string xrefs for `SE_PK_FX_ATTACK_TYPE_BUG` and `SE_PK_FX_EOB_POKEDEX_PAGE_TURN` both resolve to `FUN_005d6b90`.
- `FUN_005d6b90` calls `FUN_006ec060(name, native_sound_id, 0xffffffff)`.
- A static AArch64 `BL` scan, adjusted for the Ghidra/file-address delta of `0x00100000`, finds exactly `25` calls to `FUN_006ec060` from this registration table.
- `rootfs-triage-wiki/scripts/extract_native_sound_registrations.py` decodes each call's string pointer and `w2` immediate into `analysis/radium-image/native_sound_id_map.tsv`.
- All `25` recovered native IDs resolve to existing `primary_object_local_id` rows in `analysis/radium-image/sound_export_manifest.tsv`.

The confirmed names currently cover:

- `SE_PK_FX_ATTACK_TYPE_BUG` through `SE_PK_FX_ATTACK_TYPE_WATER`, native IDs `196` through `213`.
- `SE_PK_FX_EOB_POKEDEX_PAGE_TURN` through `SE_PK_FX_EOB_POKEDEX_BACKGROUND`, native IDs `109` through `115`.

`analysis/radium-image/sounds-native-named/` contains hardlink aliases such as `SE_PK_FX_ATTACK_TYPE_BUG__obj0196_s0628_2ch_44100hz.wav`. The full `analysis/radium-image/sounds/` export remains complete and retains deterministic object/chunk names for the unmapped sounds.

## Catalog-Order Offset Audit

Ghidra xrefs to `FUN_006ec060` confirm that the direct native registration path is exhausted for now: `get_function_xrefs` reports `25` unconditional calls, all from `FUN_005d6b90`, plus non-call data/indirection references. A Ghidra string-anchor report for `SE_PK_FX` also finds exactly `25` native strings, all referenced from `FUN_005d6b90`.

Those `25` names form two consecutive anchor blocks when compared with the `760` recovered Godot catalog constants from `spike_game_sound_ids.gdc`:

| Anchor block | Catalog indices | Radium objects | Offset |
| --- | ---: | ---: | ---: |
| EOB Pokedex FX | `40` through `46` | `109` through `115` | `+69` |
| Attack type FX | `118` through `135` | `196` through `213` | `+78` |

Because the offsets differ, there is no confirmed global Godot-catalog-index to Radium-object-ID mapping. `rootfs-triage-wiki/scripts/build_catalog_offset_candidate_labels.py` writes `analysis/radium-image/catalog_offset_candidate_labels.tsv` as a candidate-only audit table. Current output has `225` rows: `25` confirmed anchors, `199` unpromoted neighboring offset candidates, and `1` disagreement with the sound-test-adjacent table. `build_best_named_sound_aliases.py` currently promotes only the `196` unpromoted candidates whose previous best source was a plain main-image object reference, and marks them with `name_source=catalog_offset_candidate` plus `confidence=candidate_unpromoted`.

Important caution: under the EOB block's `+69` offset, object `184` would be `SE_PK_FX_BATTLE_TOTAL_FANFARE`; the sound-test-adjacent candidate table labels numeric field `184` as `SOUND: SUPER SKILL SHOT`. Since the sound-test table already conflicts with ASR/Pokedex evidence elsewhere, this does not disprove the offset candidate, but it is enough to keep that conflicting row out of `analysis/all-sound-best-name-manifest.tsv` until another native/script call-site confirms it.

## Godot GDC String Decoding

The exported Godot `.gdc` scripts are not plain text, but the format is now partially decoded enough for string/reference triage:

- Each file begins with `GDSC`; byte offset `0x0c` starts a Zstandard payload (`28 b5 2f fd`).
- The decompressed payload contains both plain ASCII strings and identifiers XOR-obfuscated with byte `0xb6` in a UTF-32LE-like layout.
- `rootfs-triage-wiki/scripts/extract_godot_gdc_strings.py` recovers both forms without executing target AArch64 binaries.
- The current run processed `93` `.gdc` scripts and wrote `6,682` recovered token rows plus `772` validated sound-reference rows.
- `spike_game_sound_ids.gdc` contributes all `760` known `SE_*` catalog constants.
- Only two scene scripts reference concrete `SpikeSoundIds` constants directly: `pokedex_slide_out.gdc` and `pokedex_slide_out_home.gdc`; both reference the same six Pokedex EOB sounds.
- `full_sceeen_fx.gdc` contains the dynamic prefix `SE_PK_FX_ATTACK_TYPE_`, but no complete validated sound constant. This is retained in `gdc_strings.tsv` and excluded from `gdc_sound_refs.tsv`.

The GDC pass confirms that the currently direct Godot scene call sites only cover a subset already mapped by the native bridge table. It does not yet provide a broad name-to-object map for the other catalog entries.

## WAV Preview Evidence

The parser can wrap selected section 8 chunks as RIFF/WAV without changing the PCM payload.

Sample rate evidence:

- Static disassembly around raw file/objdump address `0x83f638` calls `snd_pcm_hw_params_set_channels` with `2`.
- Address `0x83f644` loads `0xac44` (`44100`) before the call at `0x83f64c` to `snd_pcm_hw_params_set_rate`.

Preview validation:

```sh
file analysis/radium-image/wav-preview/*.wav
ffprobe -v error -show_entries stream=codec_name,codec_type,sample_rate,channels,duration -of compact=p=0:nk=1 analysis/radium-image/wav-preview/section8_0000_1ch_44100hz.wav
```

Observed output shape:

- `file(1)` reports `RIFF (little-endian) data, WAVE audio, Microsoft PCM, 16 bit`.
- `ffprobe` reports `pcm_s16le`, audio stream, `44100` Hz, with mono or stereo channels matching the decoded multiplier.

The `44100` Hz choice is supported by the native ALSA initialization path. The per-chunk table itself does not appear to store a separate sample rate.

## Remaining Work

Confirmed format pieces:

- mmap image header offsets
- section 8 table location and row size
- section 8 CRC32 rule
- PCM chunk offset and byte-length formula
- static section 5 command-stream references from local object IDs to every section 8 chunk
- generic section 5 opcode stream boundaries, histograms, and native opcode length/handler table dumps for both images
- section 3 pointer-table count and raw indexed bitmap preview extraction for `1449` records
- 16-bit PCM WAV wrapping for preview extraction
- full WAV export with deterministic object/chunk names
- Ghidra-confirmed native aliases for 25 Godot bridge sound names
- sound-test-adjacent candidate aliases for 4 raw native label rows, retained for audit only
- local ASR draft labels for 1,705 bounded voice-like exports
- low-confidence ASR review candidate aliases for 34 otherwise structural main-game exports
- aligned Pokedex voice aliases for 1,137 description/name/category clips
- SPIKE menu section 8 parse, 300 menu command references, and 54 exported menu WAVs

Still unresolved:

- The main-game export still has `295` structural object fallbacks, `196` catalog-offset candidate aliases, `34` low-confidence ASR review aliases, `583` raw ASR-draft names, and `220` catalog-matched ASR inferred names that still need native/script call-site confirmation.
- The section 3 compact payload subtype has `28` undecoded records, mostly compact `128 x 32` payloads; the final palette/alpha semantics for raw indexed previews also still need runtime confirmation.
- Section 4 and non-sound section 5/6 command semantics still need naming and complete structure recovery; the new opcode table dump gives native handler addresses, but most handlers still need separate decompilation/call-site interpretation.
- `/games/pokemon_pro/spike3/spike_menu/game` still needs its object metadata table recovered; the menu WAVs are associated with section 5 local object references, and 4 exact payload matches inherit stronger main-image ASR names.
