1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
|
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>6. The soundfont lookup — Animalese</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<nav class="crumb"><a href="index.html">Animalese pipeline</a> / 6. The soundfont lookup</nav>
<h1>6. The soundfont lookup</h1>
<p class="lede">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.</p>
<h2>Four tables, not one</h2>
<p>Everything is driven by tables in <code>src/static/jaudio_NES/game/audioheaders.c</code>,
which is compiled into the DOL. <strong>None of it lives in
<code>audiorom.img</code></strong> — the image is pure payload with no table of contents,
which is why extraction needs the decomp.</p>
<table>
<tr><th>Table</th><th>Entries</th><th>Indexes into</th></tr>
<tr><td><code>AudiodataHeaderStart</code></td><td>3</td><td><code>audiorom.img</code> — where the three regions are</td></tr>
<tr><td><code>AudioseqHeaderStart</code></td><td>249</td><td>the Audioseq region</td></tr>
<tr><td><code>AudiobankHeaderStart</code></td><td>159</td><td>the Audiobank region</td></tr>
<tr><td><code>AudiowaveHeaderStart</code></td><td>6</td><td>the Audiowave region</td></tr>
</table>
<p>All four are <code>ArcHeader</code>s over a common entry type
(<code>include/jaudio_NES/audiostruct.h:74</code>):</p>
<pre>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 */</pre>
<p><code>addr</code> and <code>size</code> are the address spec. <code>param0</code>–<code>param2</code>
are unused for sequences and waves. For <strong>banks</strong> they carry metadata the
loader needs <em>before</em> it can parse the blob, since the blob has no header of its
own — <code>__SetVlute</code> (<code>internal/system.c:1171</code>):</p>
<pre>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;</pre>
<p>So bank 156's <code>param0 = 0x05FF</code> means "my samples are in wave bank 5, there is
no second wave bank (<code>0xFF</code>)", and <code>param1 = 0x7700</code> means "119
instruments, 0 drums". <strong>The instrument count is a number in a table entry, not an
address.</strong></p>
<div class="note">
<strong>Alias entries.</strong> <code>__Link_BankNum</code>
(<code>internal/system.c:772</code>) reads
<code>if (header->entries[id].size == 0) id = header->entries[id].addr;</code>
— a zero-size entry is not empty, its <code>addr</code> field is reinterpreted as
<em>another entry's id</em>. Any tool walking these tables has to follow the redirect or
it will read garbage for the aliased ids.
</div>
<h2>Sequence → bank is a fifth lookup</h2>
<p><code>AudiomapHeaderStart</code> (<code>audioheaders.c:4221</code>) is a <code>u8[]</code> that
indexes itself:</p>
<pre>u16 offset[seq_id] // absolute offset into this same array
at that offset: u8 count, then u8 bank_id[count]</pre>
<p>The voice sequences sit at the tail: <code>1, 0x9C</code> / <code>1, 0x9D</code> /
<code>1, 0x9E</code> — one bank each, 156 / 157 / 158.</p>
<p>The sequence's own <code>set_bank</code> command does not carry a bank id. It carries an
index into <em>its own</em> list, counted from the end
(<code>internal/track.c:1399</code>):</p>
<pre>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;</pre>
<p>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.</p>
<h2>Inside the bank blob</h2>
<p>Pure offsets, all relative to the blob's own start:</p>
<pre>u32 drumListOffset
u32 sfxListOffset
u32 instrumentOffset[num_instruments] ← count came from param1</pre>
<p>Each instrument is a <code>voicetable</code>
(<code>audiostruct.h:348</code>, 0x20 bytes):</p>
<pre>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;</pre>
<p>and each sample descriptor a <code>smzwavetable</code> (0x10):</p>
<pre>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;</pre>
<p>with <code>adpcmbook = { s32 order, s32 n_predictors, s16 codebook[] }</code> and
<code>adpcmloop = { u32 loop_start, loop_end, count, sample_end, s16 state[16] if count > 0 }</code>.</p>
<h2>Relocation at load time</h2>
<p><code>Nas_BankOfsToAddr_Inner</code> (<code>internal/system.c:813</code>) walks the blob once
and rewrites every offset into a real pointer in place, guarded by
<code>is_relocated</code> because an instrument can appear in the list more than once.
Its per-sample helper <code>__WaveTouch</code> (<code>:1692</code>) contains the one asymmetry
that matters:</p>
<pre>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
}</pre>
<p><strong>Loop points and the VADPCM codebook live in the bank; the PCM lives in a
different region entirely.</strong> That is the whole reason <code>wave_bank_id0</code> exists —
it names which of the six wave banks to add. <code>wave0_p</code> / <code>wave1_p</code>
correspond to the two halves of <code>param0</code>.</p>
<p>Two small quirks in the same function: the relocator clamps to
<strong>126 instruments</strong> regardless of what <code>param1</code> claimed (no voice bank
comes close), and it decides "not yet relocated" by testing
<code>(u32)wavetable <= OS_BASE_CACHED</code> — small values are offsets, large ones are
already pointers.</p>
<h2>The runtime chain, for one syllable</h2>
<pre>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</pre>
<p><code>ProgToVp</code> is the only consumer of the instrument number, and it is a plain
array index bounded by <code>num_instruments</code>: on an out-of-range instrument the
bounds check fails, <code>audio_error_flags</code> is set to
<code>0x03000000 + (prog << 8) + inst</code>, and nothing plays. In retail data the
guard never trips — each sequence's dynamic table only names instruments its own bank
has. In particular <code>you</code> on bank 157 is not an error case: sequence 244 maps
code <code>0x77</code> to instrument 82, the last instrument of the bank
(see <a href="disc.html">the disc page</a>).</p>
<p>Every voice instrument uses only the <em>normal</em> slot, so <code>NoteToVoice</code> is a
formality here.</p>
<h2>Worked example — bank 156</h2>
<pre>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</pre>
<p>All three voice banks share <code>param0 = 0x05FF</code>, so all animalese samples —
119 + 83 + 119 instrument slots — point into that one 4 MB wave bank.</p>
<div class="note">
<strong>Field naming.</strong> Earlier working notes guessed
<code>u8 loaded, rangeLo, rangeHi, releaseRate</code> for the instrument header. The decomp
calls them <code>is_relocated</code>, <code>normal_range_low</code>,
<code>normal_range_high</code>, <code>adsr_decay_idx</code>. Same layout, and
<code>is_relocated</code> is a load-time scratch flag rather than anything meaningful in
the file.
</div>
<p>Decoding the VADPCM frames themselves is covered on
<a href="disc.html">the disc page</a> — including the one easy mistake, which is
convolving the decoded outputs instead of the scaled residuals.</p>
<div class="pager">
<span><a href="playback.html">← 5. Playback</a></span>
<span><a href="editor.html">7. The editor path →</a></span>
</div>
</body>
</html>
|