From 46227e2003de28202cc4e82bca4f7f4745476c4e Mon Sep 17 00:00:00 2001 From: Alexander Kassouni Date: Sat, 5 Sep 2026 17:51:13 -0700 Subject: init --- docs/soundfont.html | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/soundfont.html (limited to 'docs/soundfont.html') diff --git a/docs/soundfont.html b/docs/soundfont.html new file mode 100644 index 0000000..c07caf9 --- /dev/null +++ b/docs/soundfont.html @@ -0,0 +1,226 @@ + + + + + +6. The soundfont lookup — Animalese + + + + + + +

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 → +
+ + + -- cgit v1.3.1