6. The soundfont lookup

How a phoneme code becomes bytes on disk. Sequence and bank ids are address lookups; instrument numbers are not — they resolve through offsets stored inside the bank blob itself.

Four tables, not one

Everything is driven by tables in src/static/jaudio_NES/game/audioheaders.c, which is compiled into the DOL. None of it lives in audiorom.img — the image is pure payload with no table of contents, which is why extraction needs the decomp.

TableEntriesIndexes into
AudiodataHeaderStart3audiorom.img — where the three regions are
AudioseqHeaderStart249the Audioseq region
AudiobankHeaderStart159the Audiobank region
AudiowaveHeaderStart6the Audiowave region

All four are ArcHeaders over a common entry type (include/jaudio_NES/audiostruct.h:74):

typedef struct ArcEntry_ {
    u32 addr;         /* offset within this region */
    s32 size;
    s8  medium;       /* MEDIUM_RAM 0, DISK 1, CART 2, DISK_DRIVE 3 */
    s8  cacheType;
    s16 param0, param1, param2;
} ArcEntry;           /* 0x10 */

addr and size are the address spec. param0–param2 are unused for sequences and waves. For banks they carry metadata the loader needs before it can parse the blob, since the blob has no header of its own — __SetVlute (internal/system.c:1171):

vinfo->wave_bank_id0   = (entry->param0 >> 8) & 0xFF;
vinfo->wave_bank_id1   = (entry->param0 >> 0) & 0xFF;
vinfo->num_instruments = (entry->param1 >> 8) & 0xFF;
vinfo->num_drums       = (entry->param1 >> 0) & 0xFF;
vinfo->num_sfx         =  entry->param2;

So bank 156's param0 = 0x05FF means "my samples are in wave bank 5, there is no second wave bank (0xFF)", and param1 = 0x7700 means "119 instruments, 0 drums". The instrument count is a number in a table entry, not an address.

Alias entries. __Link_BankNum (internal/system.c:772) reads if (header->entries[id].size == 0) id = header->entries[id].addr; — a zero-size entry is not empty, its addr field is reinterpreted as another entry's id. Any tool walking these tables has to follow the redirect or it will read garbage for the aliased ids.

Sequence → bank is a fifth lookup

AudiomapHeaderStart (audioheaders.c:4221) is a u8[] that indexes itself:

u16 offset[seq_id]              // absolute offset into this same array
  at that offset:  u8 count, then u8 bank_id[count]

The voice sequences sit at the tail: 1, 0x9C / 1, 0x9D / 1, 0x9E — one bank each, 156 / 157 / 158.

The sequence's own set_bank command does not carry a bank id. It carries an index into its own list, counted from the end (internal/track.c:1399):

cmdArgU16 = ((u16*)AG.map_header)[grp->seq_id];
lo_bits   = ((u8*)AG.map_header)[cmdArgU16];               // count
// inverse lookup, 1 = last bank, 2 = second to last, …
cmdArgU8  = ((u8*)AG.map_header)[cmdArgU16 + lo_bits - cmdArgU8];
if (Nas_SzCacheCheck(BANK_TABLE, CACHE_EITHER, cmdArgU8))
    subtrack->bank_id = cmdArgU8;

That indirection lets a sequence be re-pointed at a different bank by editing the map alone. For the voice sequences the list has one entry, so the byte is always 1.

Inside the bank blob

Pure offsets, all relative to the blob's own start:

u32 drumListOffset
u32 sfxListOffset
u32 instrumentOffset[num_instruments]      ← count came from param1

Each instrument is a voicetable (audiostruct.h:348, 0x20 bytes):

typedef struct voicetable_ {
    u8 is_relocated;
    u8 normal_range_low;
    u8 normal_range_high;
    u8 adsr_decay_idx;
    envdat* envelope;                   /* offset → pointer */
    wtstr low_pitch_tuned_sample;       /* { smzwavetable*, f32 tuning } */
    wtstr normal_pitch_tuned_sample;
    wtstr high_pitch_tuned_sample;
} voicetable;

and each sample descriptor a smzwavetable (0x10):

typedef struct smzwavetable_ {
    u32 bit31 : 1;
    u32 codec : 3;        /* 0 = CODEC_ADPCM (N64 VADPCM) */
    u32 medium : 2;
    u32 bit26 : 1;
    u32 is_relocated : 1;
    u32 size : 24;
    u8*       sample;     /* offset into the WAVE bank */
    adpcmloop* loop;      /* offset within THIS bank */
    adpcmbook* book;      /* offset within THIS bank */
} smzwavetable;

with adpcmbook = { s32 order, s32 n_predictors, s16 codebook[] } and adpcmloop = { u32 loop_start, loop_end, count, sample_end, s16 state[16] if count > 0 }.

Relocation at load time

Nas_BankOfsToAddr_Inner (internal/system.c:813) walks the blob once and rewrites every offset into a real pointer in place, guarded by is_relocated because an instrument can appear in the list more than once. Its per-sample helper __WaveTouch (:1692) contains the one asymmetry that matters:

wavetable->loop = (adpcmloop*)((u32)wavetable->loop + ram_addr);   // + bank base
wavetable->book = (adpcmbook*)((u32)wavetable->book + ram_addr);   // + bank base

switch (wavetable->medium) {
    case MEDIUM_RAM:  wavetable->sample += (u32)wave_media->wave0_p; break;  // + WAVE base
    case MEDIUM_DISK: wavetable->sample += (u32)wave_media->wave1_p; break;
    case MEDIUM_CART: case MEDIUM_DISK_DRIVE: break;                         // already absolute
}

Loop points and the VADPCM codebook live in the bank; the PCM lives in a different region entirely. That is the whole reason wave_bank_id0 exists — it names which of the six wave banks to add. wave0_p / wave1_p correspond to the two halves of param0.

Two small quirks in the same function: the relocator clamps to 126 instruments regardless of what param1 claimed (no voice bank comes close), and it decides "not yet relocated" by testing (u32)wavetable <= OS_BASE_CACHED — small values are offsets, large ones are already pointers.

The runtime chain, for one syllable

Sou_VoiceStart(subtrack, 0x28)          write phoneme 0x28 ("th") to port 0
  ↓
sequence 243 spins on the port, then set_dyntbl / dyntbl_call
dyntbl[0x28] → routine "C1 27 / 88 <note> / FF"
  ↓                     C1 = set instrument 0x27   ← where code − 1 happens
Nas_ProgramChanger(subtrack, 0x27, …)                      track.c:1159
  ↓
ProgToVp(subtrack->bank_id, 0x27)                          channel.c:368
  ↓   if (inst >= voice_info[prog].num_instruments) → error, NULL
  voice_info[156].instruments[0x27]  →  voicetable*
  ↓
NoteToVoice(vtbl, note)                                    channel.c:354
  ↓   note < normal_range_low  → low sample
  ↓   note <= normal_range_high → normal sample
  ↓   else                      → high sample
wtstr { smzwavetable*, tuning }  →  sample, loop, book, codec, size

ProgToVp is the only consumer of the instrument number, and it is a plain array index bounded by num_instruments. This is exactly why you (instrument 118) cannot work on bank 157's 83 instruments: the bounds check fails, audio_error_flags is set to 0x03000000 + (prog << 8) + inst, and nothing plays — though in practice the sequence's dynamic table has already clamped the index before it gets there.

Every voice instrument uses only the normal slot, so NoteToVoice is a formality here.

Worked example — bank 156

audiorom.img                              AudiodataHeaderStart
├ Audioseq   @ 0x000000  len 0x0CF700     entry 0
├ Audiobank  @ 0x0CF700  len 0x067C80     entry 1
│   └ bank 156  @ +0x05C260 → 0x12B960   len 0x4500   AudiobankHeaderStart[156]
│       ├ u32[0] drumList  = 0            (num_drums = 0)
│       ├ u32[1] sfxList   = 0            (num_sfx   = 0)
│       ├ u32[2..120]  119 instrument offsets
│       └ voicetables · envelopes · smzwavetables · loops · codebooks
└ Audiowave  @ 0x137380  len 0x6B33E0     entry 2
    └ wave bank 5 @ +0x2C5DC0 → 0x3FD140  len 0x3ED620  AudiowaveHeaderStart[5]
        └ raw VADPCM frames; smzwavetable.sample is an offset into here

All three voice banks share param0 = 0x05FF, so all animalese samples — 119 + 83 + 119 instrument slots — point into that one 4 MB wave bank.

Field naming. ../extraction.md §2.2 guessed u8 loaded, rangeLo, rangeHi, releaseRate for the instrument header. The decomp calls them is_relocated, normal_range_low, normal_range_high, adsr_decay_idx. Same layout, and is_relocated is a load-time scratch flag rather than anything meaningful in the file.

Decoding the VADPCM frames themselves is covered in ../extraction.md §2.3 — including the one easy mistake, which is convolving the decoded outputs instead of the scaled residuals.

← 5. Playback 7. The editor path →