diff options
Diffstat (limited to 'docs/soundfont.html')
| -rw-r--r-- | docs/soundfont.html | 226 |
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->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>. 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 << 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> |