aboutsummaryrefslogtreecommitdiff
path: root/docs/soundfont.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/soundfont.html')
-rw-r--r--docs/soundfont.html226
1 files changed, 226 insertions, 0 deletions
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 @@
+<!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-&gt;wave_bank_id0 = (entry-&gt;param0 &gt;&gt; 8) &amp; 0xFF;
+vinfo-&gt;wave_bank_id1 = (entry-&gt;param0 &gt;&gt; 0) &amp; 0xFF;
+vinfo-&gt;num_instruments = (entry-&gt;param1 &gt;&gt; 8) &amp; 0xFF;
+vinfo-&gt;num_drums = (entry-&gt;param1 &gt;&gt; 0) &amp; 0xFF;
+vinfo-&gt;num_sfx = entry-&gt;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-&gt;entries[id].size == 0) id = header-&gt;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-&gt;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-&gt;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 &gt; 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-&gt;loop = (adpcmloop*)((u32)wavetable-&gt;loop + ram_addr); // + bank base
+wavetable-&gt;book = (adpcmbook*)((u32)wavetable-&gt;book + ram_addr); // + bank base
+
+switch (wavetable-&gt;medium) {
+ case MEDIUM_RAM: wavetable-&gt;sample += (u32)wave_media-&gt;wave0_p; break; // + WAVE base
+ case MEDIUM_DISK: wavetable-&gt;sample += (u32)wave_media-&gt;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 &lt;= 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 &lt;note&gt; / FF"
+ ↓ C1 = set instrument 0x27 ← where code − 1 happens
+Nas_ProgramChanger(subtrack, 0x27, …) track.c:1159
+ ↓
+ProgToVp(subtrack-&gt;bank_id, 0x27) channel.c:368
+ ↓ if (inst &gt;= voice_info[prog].num_instruments) → error, NULL
+ voice_info[156].instruments[0x27] → voicetable*
+ ↓
+NoteToVoice(vtbl, note) channel.c:354
+ ↓ note &lt; normal_range_low → low sample
+ ↓ note &lt;= 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>. This is exactly why
+<code>you</code> (instrument 118) cannot work on bank 157's 83 instruments: the bounds
+check fails, <code>audio_error_flags</code> is set to
+<code>0x03000000 + (prog &lt;&lt; 8) + inst</code>, and nothing plays — though in practice the
+sequence's dynamic table has already clamped the index before it gets there.</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> <code>../extraction.md</code> §2.2 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 in <code>../extraction.md</code> §2.3 —
+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>