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.
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.
| Table | Entries | Indexes into |
|---|---|---|
AudiodataHeaderStart | 3 | audiorom.img — where the three regions are |
AudioseqHeaderStart | 249 | the Audioseq region |
AudiobankHeaderStart | 159 | the Audiobank region |
AudiowaveHeaderStart | 6 | the 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.
__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.
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.
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 }.
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.
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.
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.
../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.