From 46227e2003de28202cc4e82bca4f7f4745476c4e Mon Sep 17 00:00:00 2001 From: Alexander Kassouni Date: Sat, 5 Sep 2026 17:51:13 -0700 Subject: init --- .envrc | 1 + .gitignore | 3 + Cargo.lock | 186 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 7 ++ docs/editor.html | 116 +++++++++++++++++++++++ docs/encoding.html | 136 +++++++++++++++++++++++++++ docs/gating.html | 180 ++++++++++++++++++++++++++++++++++++ docs/index.html | 101 ++++++++++++++++++++ docs/phonology.html | 235 +++++++++++++++++++++++++++++++++++++++++++++++ docs/playback.html | 196 +++++++++++++++++++++++++++++++++++++++ docs/quirks.html | 157 +++++++++++++++++++++++++++++++ docs/reference.html | 217 +++++++++++++++++++++++++++++++++++++++++++ docs/scheduling.html | 147 +++++++++++++++++++++++++++++ docs/soundfont.html | 226 +++++++++++++++++++++++++++++++++++++++++++++ docs/style.css | 83 +++++++++++++++++ flake.lock | 27 ++++++ flake.nix | 41 +++++++++ src/config.rs | 0 src/main.rs | 20 ++++ src/phonemes/mod.rs | 0 src/phonemes/phonemes.rs | 6 ++ src/tokenizer/mod.rs | 2 + src/tokenizer/tokens.rs | 74 +++++++++++++++ 23 files changed, 2161 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 docs/editor.html create mode 100644 docs/encoding.html create mode 100644 docs/gating.html create mode 100644 docs/index.html create mode 100644 docs/phonology.html create mode 100644 docs/playback.html create mode 100644 docs/quirks.html create mode 100644 docs/reference.html create mode 100644 docs/scheduling.html create mode 100644 docs/soundfont.html create mode 100644 docs/style.css create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/phonemes/mod.rs create mode 100644 src/phonemes/phonemes.rs create mode 100644 src/tokenizer/mod.rs create mode 100644 src/tokenizer/tokens.rs diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..052de26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/result +/.direnv diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..6141f69 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,186 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "animalese" +version = "0.1.0" +dependencies = [ + "clap", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..349f6be --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "animalese" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.6.6", features = ["derive"] } diff --git a/docs/editor.html b/docs/editor.html new file mode 100644 index 0000000..8e46242 --- /dev/null +++ b/docs/editor.html @@ -0,0 +1,116 @@ + + + + + +7. The editor path — Animalese + + + + + + +

7. The editor path

+

There is a third calling convention into Na_VoiceSe that skips +the phonology engine entirely, and it explains what a third of the voice samples are +for.

+ +

Mode 1

+ +

The text editor overlay — used for letters, town and player names, patterns, and +anywhere else the player types — speaks each keystroke from +src/game/m_editor_ovl.c:712:

+ +
if (command == mED_COMMAND_OUTPUT_CODE) {
+    int voice = mMsg_sound_voice_get_for_editor(editor_ovl->now_code);
+
+    if (voice == 0x85 || voice == 0x80 || voice == 0x81 ||
+        voice == 0x82 || voice == 0x83 || voice == -1)
+        voice = 0x86;
+
+    sAdo_VoiceSe(voice, 0xFF, 0xFF, 0, 32, 1);      // ← mode 1
+}
+else if (command == mED_COMMAND_EXCHANGE_CODE) {    // cursor moved onto a character
+    if (exchange_code != -1 && mED_exchange_se_list[exchange_code])
+        sAdo_VoiceSe(mMsg_sound_voice_get_for_editor(exchange_code), 0xFF, 0xFF, 0, 32, 1);
+    else
+        sAdo_SysTrgStart(0x36);
+}
+ +

After both filters, only three things survive: letters (0x5D–0x76), +digits (0x53–0x5C), and the click (0x86).

+ +

What mode 1 does inside Na_VoiceSe

+ +

Na_VoiceSe's two switch (f) blocks handle only cases 0 and 2. +With f == 1 both fall through, so:

+ + + +

The editor always runs at spec 5 or 6 (see gating), which +routes 0x86 to NA_SE_BEBE and passes everything else through. The +final guard is if (a != 0 && a <= 0x77), and letters and digits both +satisfy it. So:

+ +
typing 'a'  →  Sou_VoiceStart(subtrack, 0x5D)  →  instrument 0x5C
+typing 'z'  →  Sou_VoiceStart(subtrack, 0x76)  →  instrument 0x75
+typing '0'  →  Sou_VoiceStart(subtrack, 0x53)  →  instrument 0x52
+ +

What this tells us about the sample set

+ +

Trace the reachable phoneme codes across the whole engine:

+ + + + + + + +
Code rangeInstrumentsReached by
0x01–0x520–81the phonology engine — short/long vowels, consonants, all 52 two-character digraphs plus you
0x01–0x40also the anti-repetition random substitution (rand() >> 2)
0x53–0x7682–117only mode 1 — one entry per digit and letter, in order
0x77118the you digraph, and nothing else
+ +

The 0x53–0x76 block is exactly 36 codes: ten digits then +twenty-six letters, in the same order as the voice-code alphabet, and it is +unreachable through any dialogue. The obvious reading is that these 36 samples are the +spoken names of the digits and letters — what you hear when typing a name +one character at a time.

+ +
+Confidence. The code path is unambiguous; the interpretation of the samples +as letter names is inference. It is testable: extract instruments 82–117 from bank 156 +and listen. It also predicts that bank 157, which stops at 83 instruments, is a +phonology-only bank — and that is exactly what it is. +
+ +

Why bank 157 is short

+ +

Spec 3 (Wendell, Pete, Booker, Jack) uses sequence 244 / bank 157, which has 83 +instruments — codes 0x01–0x53. That covers the entire phonology +output range (≤ 0x52) with one to spare, and omits the letter-name block +that spec 3 can never reach, since the editor is always spec 5 or 6.

+ +

The one real casualty is you (0x77): the sequence's dynamic +table clamps out-of-range entries to the last routine, so a spec-3 voice saying "you" +plays instrument 82 instead. That is one wrong syllable on a common word, in four +NPCs' voices.

+ +
+This corrects an earlier reading of extraction.md §2.4, which described bank 157 +as "collapsing every digraph sample above 0x53 onto one sound". Nothing above +0x53 is a digraph — the block is not phonology at all. +
+ +
+ ← 6. The soundfont lookup + 8. Quirks & corrections → +
+ + + diff --git a/docs/encoding.html b/docs/encoding.html new file mode 100644 index 0000000..8b57261 --- /dev/null +++ b/docs/encoding.html @@ -0,0 +1,136 @@ + + + + + +2. Text → voice code — Animalese + + + + + + +

2. Text → voice code

+

A flat 256-entry table turns the game's own character encoding into a +34-value "voice code" alphabet. This is the only place text is looked at.

+ +

The game's character set

+ +

Messages are stored one byte per character in a custom encoding +(include/m_font.h). It is ASCII-compatible over exactly three ranges — +0-9 at 48–57, A-Z at 65–90, a-z at 97–122 — and +diverges everywhere else. Notable non-ASCII placements:

+ + + + + + + + + + + +
CodeCharacterNote
0–31uppercase accented Latin, ¡ ¿ Ç Ð Ñ ß Þwhere ASCII control codes would be
43♥where ASCII has +
47♪where ASCII has /
59droplet symbolwhere ASCII has ;
92annoyed symbolwhere ASCII has \
127CHAR_CONTROL_CODEescape byte; a control code follows
128CHAR_MESSAGE_TAGescape byte
210, 211short space, wide spacelayout-only spaces
+ +

The voice table

+ +

mMsg_sound_voice_get(code) (src/game/m_msg_sound.c_inc:1) is a +256-byte lookup with two hard-coded exceptions:

+ +
if (code == CHAR_CONTROL_CODE || code == CHAR_MESSAGE_TAG)
+    return -1;                      // no sound, and no state change
+return voice_array[code];
+ +

Those are the only two entries in the array holding 0xFF, so +0xFF is never actually returned from this function — it arrives at +Na_VoiceSe only as a padding value for the unused b/c +slots.

+ +

Voice code space

+ + + + + + + + + + + + + +
Voice codeMeaning
0x53–0x5Cdigits 0–9
0x5D–0x76letters a–z
0x80♪ — plays the speaker's melody, bypasses everything
0x81space
0x82? — raises pitch
0x83! — raises volume
0x84newline (injected, never in the table)
0x85mute — the catch-all for punctuation and symbols
0x86click — plays NA_SE_BEBE
0xFFempty slot in a window
+ +

The split at 0x77 matters: Sou_BoinShiinCheck and several rules in +Na_VoiceSe test > 0x77 to mean "this is punctuation, not a letter".

+ +

Case and accent folding

+ +

Upper and lower case map to the same code, and every accented Latin letter folds onto +its base letter. Some of the folds are worth knowing about:

+ + + + + + + + + + + + + + + +
Character(s)Folds to
À Á Â Ã Å à á â ã ä å, ª (feminine ordinal)a
ß (eszett)b — a glyph-shape fold, not a phonetic one
Ç çc
Ð ðd
È É Ê Ë è é ê ëe
Ì Í Î Ï ì í î ïi
Ñ ñn
Ò Ó Ô Õ Ö ò ó ô õ ö, º (masculine ordinal)o
Þ þ (thorn)p
Ù Ú Û Ü ù ú û üu
Ý ý ÿy
Œ œmute — the only accented Latin pair that is not folded
+ +

Everything else

+ + + +

Two wrappers

+ +

mMsg_sound_voice_get2

+ +
if (code == CHAR_NEW_LINE) return 0x84;
+return mMsg_sound_voice_get(code);
+ +

Newline has no entry in the table; the wrapper injects 0x84. This is the +function the scheduler actually calls, so 0x84 reaches the engine while the +raw table never produces it. m_msg_sound.c_inc:44

+ +

mMsg_sound_voice_get_for_editor

+ +
sound = mMsg_sound_voice_get(code);
+switch (sound) {
+    case 0x80: case 0x82: case 0x83: case 0x85: return 0x86;
+    default: return sound;
+}
+ +

In the text editor, ♪, ?, ! and every mute character all become +the click. See the editor path. +m_msg_sound.c_inc:26

+ +
+Only these 34 values leave the text layer. Word boundaries, capitalisation, +sentence structure and the actual letters beyond a-z0-9 are all gone by +the time audio sees anything. Whatever "understanding of English" the animalese has, +it lives entirely in the digraph table on the next page. +
+ +
+ ← 1. Gating + 3. Windowing & timing → +
+ + + diff --git a/docs/gating.html b/docs/gating.html new file mode 100644 index 0000000..9e78ac0 --- /dev/null +++ b/docs/gating.html @@ -0,0 +1,180 @@ + + + + + +1. Gating & voice selection — Animalese + + + + + + +

1. Gating & voice selection

+

Before a single character is converted, two decisions are made: whether +this message speaks at all, and with whose voice.

+ +

Voice mode

+ +

There are three voice modes, pushed to the audio engine by +sAdo_SetVoiceMode and stored in sou_voice_mode:

+ + + + + + +
ModeNameBehaviour
0VOICE_MODE_ANIMALESEfull phonology, samples from the voice bank
1VOICE_MODE_CLICKevery non-space character plays NA_SE_BEBE, one blip
2VOICE_MODE_SILENTnothing
+ +

The mode is not simply the player's config setting. It is the product of the setting +and of who is talking — mMsg_sound_voice_mode_get:

+ +
mode_type = Save_Get(config).voice_mode        // 0 animalese, 1 bebese, 2 silent
+if (msg has VOICE_SILENT flag) mode_type = SILENT
+
+if (mMsg_check_sound_animal(msg))
+    mode = { ANIMALESE, CLICK, SILENT }[mode_type]   // an animal is speaking
+else if (mode_type == SILENT)
+    mode = SILENT
+else
+    mode = CLICK                                     // narration, signs, menus
+ +

Source: src/game/m_msg_sound.c_inc:259

+ +

So the config's "animalese" setting only unlocks animalese for messages that pass +mMsg_check_sound_animal. Everything else — item descriptions, bulletin +boards, system prompts — is click mode no matter what.

+ +

What counts as an animal talking

+ +

mMsg_check_sound_animal (m_msg_sound.c_inc:66) returns true when the +VOICE_CLICK flag is clear and any of:

+ + + +

The special ranges exist so that disembodied event narration still gets a voice. +They are forced to spec 4 regardless of any actor +(mMsg_sound_spec_change_voice_force, :288).

+ +

Spec: which of the three voices

+ +

The speaker maps to a "spec" (2–9), and the spec selects a sequence and bank. +Na_SpecChange (game64.c_inc:3459) is the switch, and it is a no-op +if the spec is unchanged — otherwise it stops the voice sequence, clears the stay +cache, and starts a new one:

+ + + + + + +
SpecSequenceBankInstrumentsUsed by
2, 5, 7, 9243156119default villagers, most specials, male player, Timmy/Tommy, Farley
324415783Wendell, Pete, Booker, Jack
4, 6, 8245158119Pelly, Phyllis, Mabel, Sable, Blanca, female player, Joan, event narration
+ +

Spec also selects a base timbre inside Na_VoiceSe +(game64.c_inc:2760), reapplied on every call:

+ + + + + + + + +
SpecVolume (_08)Pitch (_14)Character
2, 3, 40.651.00baseline
5, 60.901.00player (louder, unmodulated)
70.901.30squeaky — Timmy & Tommy
80.900.75low — Joan
90.700.65very low — Farley
+ +

Note that spec is orthogonal to bank: specs 5, 7 and 9 all use bank 156 and +differ only in this pitch/volume pair. There are three sample sets and eight voices.

+ +

Villagers

+ +

mNpc_GetNpcSoundSpec (src/game/m_npc.c:4116) indexes by personality:

+ +
static int spec_table[mNpc_LOOKS_NUM] = { 4, 4, 2, 2, 3, 4 };
+//   normal(girl) 4 | peppy 4 | lazy(boy) 2 | jock 2 | cranky 3 | snooty 4
+ +

An out-of-range looks falls back to mNpc_LOOKS_BOY (spec 2).

+ +

Special NPCs

+ +

Non-villagers fall through to mNpc_GetNpcSoundSpecNotAnimal, a linear scan of +l_sp_actor_name (m_npc.c:32). The default when not found is spec 2. +Everything in that table is spec 2 except:

+ + + + + + + +
SpecNPCs
3Wendell, Pete, Booker, Jack
4Pelly, Phyllis, Mabel, Sable, Blanca
8Joan
9Farley
+ +

The player's own voice: specs 5 and 6

+ +

Specs 5 and 6 never appear in any NPC table. They are set from the submenu +(src/game/m_submenu.c:364) when a menu that can type text opens:

+ +
if ((menu_type == mSM_OVL_LEDIT && param0 == 0) ||
+    Common_Get(now_private)->gender == mPr_SEX_MALE) {
+    sAdo_SpecChange(5);          // male
+} else {
+    sAdo_SpecChange(6);          // female
+}
+sAdo_SetVoiceMode(0);            // force animalese on regardless of config
+ +

Two things follow. First, letter-writing and name entry force animalese even when the +player has set the config to click or silent. Second, specs 5 and 6 are treated as a +distinct class throughout Na_VoiceSe:

+ + + +

See the editor path for what actually gets spoken there.

+ +

Timmy and Tommy

+ +

Two text control codes toggle a temporary spec override, used for Nook's nephews:

+ + + + + +
CodeValueEffect
mFont_CONT_CODE_VOICE_TRUE92 (0x5C)restore msg_p->spec
mFont_CONT_CODE_VOICE_FALSE93 (0x5D)switch to spec 7 (pitch × 1.3)
+ +

Both guard on the actor being SP_NPC_MAMEDANUKI0/1, so the codes are inert in +any other message. m_msg_sound.c_inc:317–340

+ +

Hard gates inside Na_VoiceSe

+ +

Four early returns drop the call regardless of everything above +(game64.c_inc:2724–2735):

+ + + +
+ ← Index + 2. Text → voice code → +
+ + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..d6a0817 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,101 @@ + + + + + +Animalese pipeline — Animal Crossing (GC) + + + + +

Animalese: the text → speech pipeline

+

How Animal Crossing (GameCube, GAFE01, USA rev 0) turns a +message string into villager babble. Every claim here is traced to a function in the +ac-decomp tree.

+ +

The short version

+ +
+message text  (game charset, 1 byte/char)
+  │
+  │  1. mMsg_sound_voice_get()            256-entry table
+  ▼     char code ──► voice code (0x53–0x86)
+gating + voice selection
+  │  2. mMsg_sound_voice_mode_get()       animalese / click / silent
+  │     mNpc_GetNpcSoundSpec()            speaker ──► spec 2–9
+  ▼     Na_SpecChange()                   spec ──► sequence 243/244/245
+windowing
+  │  3. mMsg_sound_voice_entry()          collect (a, b, c) over two cursor steps
+  ▼     ~1 character per 2 game frames
+draw-time trigger
+  │  4. mFontSentence_animal_voice_se()   glyph scale ──► volume
+  ▼     sAdo_VoiceSe(a, b, c, npc, scale, mode)
+phonology
+  │  5. Na_VoiceSe()                      digraphs, long vowels, silent e
+  ▼     ──► phoneme code 0x01–0x77
+playback
+  │  6. Sou_VoiceStart()                  write port 0 of a JAudio2 subtrack
+  │     sequence dyntbl ──► instrument (code − 1)
+  ▼  7. ProgToVp(bank, inst) ──► NoteToVoice() ──► smzwavetable
+DSP  ──►  one ~190 ms VADPCM sample, 22050 Hz
+
+ +

A phoneme code is literally a soundfont instrument number plus one. The whole +"engine" is a lookup table with about fifty special cases; the character comes almost +entirely from the samples.

+ +

Pages

+ + + +

Scope

+

These pages document the runtime pipeline as decompiled. Extracting the +actual audio samples from a disc image is covered separately in +../extraction.md, which also carries the audio table-of-contents, +bank/sample formats, and VADPCM decoding notes.

+ +
+No game assets here. Everything on these pages is derived from decompiled +source. Reproducing audible animalese needs a legally dumped copy of the game. +
+ +

Verified against ac-decomp at commit 09ca8e8b (DOL and REL 100 % linked).

+ + + diff --git a/docs/phonology.html b/docs/phonology.html new file mode 100644 index 0000000..ebbc872 --- /dev/null +++ b/docs/phonology.html @@ -0,0 +1,235 @@ + + + + + +4. The phonology engine — Animalese + + + + + + +

4. The phonology engine

+

Na_VoiceSe — src/static/jaudio_NES/game/game64.c_inc:2598. +Given three voice codes it produces one phoneme code now and optionally queues a second +for the next frame.

+ +
void Na_VoiceSe(u8 a, u8 b, u8 c, s16 voice, u8 scale, u8 f)
+ +

f is the calling mode: 0 is the normal message path, +2 is the deferred second syllable, 1 is the +editor path which skips this page entirely.

+ +

Vowel / consonant classification

+ +

Sou_BoinShiinCheck (:2564) — boin = vowel, +shiin = consonant:

+ + + + + + +
ReturnsClassMembers
0vowela e i o u and n
1consonantthe other 20 letters
2neitherdigits and every punctuation code
+ +

n being a vowel is deliberate — it is a syllabic nasal in Japanese, and this +engine was written by Japanese programmers modelling English through a kana-shaped +lens. It changes when the silent-e and magic-e rules fire, and it is +the single most surprising thing in the engine.

+ +

Rule order for mode 0

+ +

State is carried in sou_last_num and sou_last_num2, the previous +call's raw a and b. The originals are also stashed in +sou_num_org/sou_num2_org/sou_num3_org before anything is +rewritten.

+ +

0. Lone mute

+
if (a == 0x85 && b == 0xFF && c == 0xFF) return;
+

Returns before touching any state, so a lone punctuation mark does not disturb the +previous-character history.

+ +

1. Silent e (:2612)

+
if (a == 'e' && b > 0x77 && class(last_num) == vowel && class(last_num2) == consonant) {
+    a = b;  r28 = 4;                      // drop the e, speak the punctuation slot
+}
+if (b == 'e' && c > 0x77 && class(last_num2) == vowel && class(a) == consonant) {
+    b = 0;  r28 = 4;                      // drop the e, queue nothing
+}
+

A word-final e preceded by vowel + consonant is dropped: make, +time, nose. Because n classifies as a vowel, done and +gone also trip it. Setting r28 = 4 skips the digraph pass entirely.

+ +

2. Digraph merge (:2623)

+
a = Sou_ConnectCheck(a, b, c);
+r28 = (a == sou_num_org) ? 1 : 2;         // 2 = merged, both letters consumed
+

If a digraph matched, r28 = 2 and control falls straight through to +playback: the merged phoneme plays and nothing is queued, so both letters +are consumed by one sample.

+ +

The digraph table — Sou_ConnectCheck (:2316)

+

53 entries. All are two characters except you, the only rule that reads c; one pairs a letter with a space rather than another letter (i␣).

+ +
+ai→0x0F
al→0x02
an→0x39
ar→0x16
as→0x3A
+be→0x3B
by→0x3C
+ch→0x08
+do→0x3D
+ea→0x15
er→0x0D
+fa→0x35
fo→0x34
+ge→0x17
go→0x3E
+ha→0x3F
he→0x40
hi→0x41
ho→0x42
+i␣→0x05
if→0x43
in→0x44
ir→0x0C
is→0x45
it→0x46
+ks→0x31
+me→0x47
my→0x48
+na→0x38
ng→0x1C
no→0x49
+of→0x4A
oh→0x4B
ok→0x4C
on→0x4D
oo→0x2A
or→0x2C
ou→0x20
ow→0x06
oy→0x1F
+re→0x0E
+se→0x37
sh→0x24
si→0x36
so→0x4F
su→0x25
+th→0x28
to→0x50
+up→0x51
+we→0x52
wo→0x33
+you→0x77
+ze→0x32 +
+ +

The design is transparent. Fourteen entries are genuine English digraphs — +ch sh th ng oo ow ou oy ai ea ar er ir or. The other thirty-nine are the +openings of common English words: he ho hi ha me my no on of oh ok so to up we +by do go is it in if as an al na se si su fa fo ge ze ks you. The engine does not +model English phonetics; it memorises the most frequent English word-starts and gives +each one a recorded syllable.

+ +

3. Punctuation flush (:2630)

+
if (b is punctuation) {
+    r28 = 3;
+    if (sou_last_num2 was also punctuation) r28 = 1;   // don't flush twice
+}
+

When a word ends, r28 = 3 re-runs the merge one character further back:

+
a = Sou_ConnectCheck(sou_last_num2, sou_num_org, sou_num2_org);
+if (a < 0x53 || a > 0x76) break;      // it merged, or wasn't a letter — leave it
+a = Sou_TanboinHenkan(a, b);
+

This gives the final letter of a word a chance to pair with the letter before it +instead of being dropped against the following space. Note the guard also drops the +whole branch if a > 0x77 before it starts.

+ +

4. Vowel length (:2658, for r28 == 1 or 4)

+
if (class(a) == vowel && class(b) == consonant && c == 'e')
+    a = Sou_ChouboinHenkan(a, b);      // long  — magic e, as in cake
+else
+    a = Sou_TanboinHenkan(a, b);       // short
+
+sou_num2_request   = b;                // queue the second syllable
+sou_num2_animal_id = voice;
+sou_num2_scale     = scale;
+ +

Chouboin = long vowel, tanboin = short vowel. The two tables are +identical except for five entries:

+ + + + + +
aiueon
short0x010x140x290x0A0x1D0x1B
long0x020x150x2A0x0E0x1E0x1B
+ +

n is the same in both — it is only classified as a vowel, it has no long form. +Note the aliasing: long a (0x02) is the same sample as digraph al, +long i (0x15) is ea and consonant y, long e +(0x0E) is re, long u (0x2A) is oo.

+ +

Consonants (both tables)

+ + + + + + + + +
LetterCodeLetterCodeLetterCodeLetterCode
b0x07g0x12m0x1At0x26
c0x23 / 0x18h0x00p0x21v0x2D
d0x09j0x17q0x18w0x2E
f0x11k0x18r0x22x0x31
l0x19s0x23y0x15
z0x30
+ +

Only one context rule exists: c is soft (0x23, same as s) when the +next letter is i or e, hard (0x18, same as k) +otherwise. h maps to 0x00, which has no instrument — h is +silent unless it merges into a digraph.

+ +

Digits

+ + + +
0123456789
0x320x330x260x270x340x350x360x370x100x38
+

Eight of the ten reuse a code the letter tables also produce, and the overlaps line up +with the first syllable of the spoken number: 0=ze(ro), +1=wo(one), 2=t(wo), +4=fo(ur), 5=fa(five), +6=si(x), 7=se(ven), +9=na(nine). Only 3 (0x27) and 8 (0x10) +get codes nothing else reaches. The digraph names here are just labels for shared +samples — the recording itself presumably says the number.

+ +

The deferred second syllable — mode 2

+ +

Sou_SpecialRoutine10 (:1708) runs once per game frame inside +Na_GameFrame:

+ +
if (sou_Na_VoiceSeFlag == 1) {
+    sou_Na_VoiceSeFlag = 0;               // a real call happened this frame
+} else if (sou_num2_request) {
+    Na_VoiceSe(sou_num2_request, 0, 0, sou_num2_animal_id, sou_num2_scale, 2);
+    sou_num2_request = 0;
+}
+ +

So the queued b plays on the first frame where no fresh call arrived. Mode 2 +is a stripped-down path:

+ +
if (sou_voice_mode == 1) return;                    // click mode: no second syllable
+if (a == 0xFF) return;
+if (a == 'e' && sou_num3_org > 0x77 &&
+    class(sou_last_num2) == vowel && class(sou_num_org) == consonant)
+    a = 0;                                          // silent-e check again
+else
+    a = Sou_TanboinHenkan(a, sou_num3_org);         // always short
+ +

The queued syllable never gets the long-vowel treatment and never re-checks digraphs, +which is why the second half of a pair always sounds flatter than the first.

+ +

Anti-repetition

+ +
// mode 0
+if (sou_num_org == sou_last_num2 && sou_num_org == sou_last_num) i = 1;
+…
+if (spec == 5 || spec == 6 || r29 > 0x77) i = 0;
+if (i == 1) a = Nap_GetRandom() >> 2;               // 0x00–0x3F
+ +

Three identical voice codes in a row — aaa, mmm, zzz — +substitute a uniformly random phoneme from the first 64. This is the only path that +reaches many of the codes never produced by any rule, and it is why repeated-letter +interjections in dialogue sound like nonsense rather than a stutter. It is disabled +for the player's voice (specs 5 and 6).

+ +

♪ — the melody escape

+ +
if (a == 0x80) {
+    if (voice == 0xFA) return;
+    switch (sou_voice_mode) {
+        case 0: Na_MelodyVoice(voice); break;      // animalese
+        case 1: Na_SysTrgStart(NA_SE_BEBE); break; // click
+        case 2: break;                             // silent
+    }
+    return;
+}
+ +

Na_MelodyVoice (src/static/jaudio_NES/game/melody.c:727) plays a +fixed 16-note pattern {13, 14 × 15} on subtrack 6 at volume 1.3, using the +speaker's npc_voice_id to select which chunk of sequence 248 to copy in. The +id is the NPC's instrument type (draw_data.voice_type), the same one used for +the town tune — so ♪ plays "their" instrument rather than a phoneme.

+ +
+ ← 3. Windowing & timing + 5. Playback → +
+ + + diff --git a/docs/playback.html b/docs/playback.html new file mode 100644 index 0000000..1cf9464 --- /dev/null +++ b/docs/playback.html @@ -0,0 +1,196 @@ + + + + + +5. Playback — Animalese + + + + + + +

5. Playback

+

A phoneme code becomes an instrument number, written into a port on a +looping JAudio2 sequence. Everything expressive happens through two float multipliers.

+ +

Punctuation, applied at play time

+ +

By the time the rule engine finishes, punctuation codes are still in a. +The sou_voice_mode switch (game64.c_inc:2810) resolves them, and +splits on whether this is the player's voice:

+ +

Specs 5 and 6 — the player

+
switch (a) {
+    case 0x81: case 0x84: case 0x85: case 0x86:
+        Na_SysTrgStart(NA_SE_BEBE);  a = 0;  break;
+}
+

Every punctuation code clicks. ? and ! fall through unmodified +into the phoneme path — the pitch and volume bumps below never apply to the player.

+ +

All other specs

+ + + + + + + +
CodeCharEffect
0x86clickNA_SE_BEBE, then mute
0x81 0x84 0x85space, newline, mutemute
0x82?mute, and pitch × 1.24 / 1.30 / 1.13 for sequence 243 / 244 / 245
0x83!mute, and volume × 1.15
anything elseplays; sets sou_now_boin (dead, see quirks)
+ +

Both multipliers are applied to the spec's base value, which was just reset at +the top of the call, so they last exactly one utterance. The pitch factor differing per +sequence is a per-bank tuning correction, not an expressive choice.

+ +

The emotion LFO

+ +

Six emotion states are set by text control codes and stored in +sou_message_status:

+ + + + + + + + + +
Control codeValueState
SET_MESSAGE_CONTENTS_NORMAL75VOICE_STATUS_NORMAL
SET_MESSAGE_CONTENTS_ANGRY76VOICE_STATUS_ANGRY
SET_MESSAGE_CONTENTS_SAD77VOICE_STATUS_SAD
SET_MESSAGE_CONTENTS_FUN78VOICE_STATUS_FUN
SET_MESSAGE_CONTENTS_SLEEPY79VOICE_STATUS_SLEEPY
SET_MESSAGE_CONTENTS_GLOOMY97VOICE_STATUS_GLOOMY
+ +

Each drives a short random-period modulation of _18 (pitch multiplier) and +_0C (volume multiplier), recomputed per utterance +(game64.c_inc:2880). It is skipped for punctuation +(r29 > 0x77) and for specs 5 and 6, in which case the previous values +persist.

+ +

Angry (1)

+
if (counter == 0) counter = (rand() >> 6) + 10;   // 10–13
+else counter--;
+switch (counter) {
+    case 0: case 1: case 2:  pitch = 1.25, vol = 1.25;  break;
+    case 3: case 4:          pitch = 1.20, vol = 1.20;  break;
+    case 9: case 10:         pitch = 1.25, vol = 1.25;  break;
+    default:                 pitch = 0.90, vol = 1.05;  break;
+}
+

Mostly flat and slightly quiet, with two bursts near the top and bottom of each +10–13 syllable cycle — a jagged, shouty contour.

+ +

Sad (2), Sleepy (4), Gloomy (5)

+
if (counter == 0) { effect = (rand() >> 6) + 14; counter = 1; }   // 14–17
+else if (counter == effect) { counter = 0; sad_toguru ^= 1; }
+else counter++;
+
+pitch = (sad_toguru ? 1.1 : 1.0) - counter * 0.022;
+vol   = (sad_toguru ? 1.1 : 1.0) - counter * 0.025;
+

A sawtooth: pitch and volume slide down over 14–17 syllables, then jump back up, +alternating between two baselines. The three states share the code exactly — they +differ only in what the writers chose to tag.

+ +

Fun (3)

+
if (counter == 0) counter = (rand() >> 6) + 10;
+else counter--;
+switch (counter) {
+    case 0: case 1:  pitch = 1.05, vol = 1.05;  break;
+    case 2:          pitch = 0.90, vol = 1.15;  break;
+    default:         pitch = 1.15, vol = 1.05;  break;
+}
+

Consistently bright with a dip at the end of each cycle.

+ +

Normal (0)

+
pitch = 1.0;  vol = 1.0;
+ +

Reaching the DSP

+ +

Two subtracks in NA_GROUP4 alternate, toggled by +sou_voice_se_toguru on every call, so consecutive syllables can overlap +rather than cutting each other off:

+ +
void Sou_VoiceStart(u8 subtrack, u8 b) {
+    NA_COMMAND_AUDIO_SUBTRACK_SET_PORT(VOICE_GROUP, subtrack, 0, b);
+}
+ +

The guard before it is if (a != 0 && a <= 0x77) — code 0 never plays, +which is why h is silent. The call is also skipped entirely when the raw +code was space, newline or mute (r29 != 0x85 && r29 != 0x81 && +r29 != 0x84).

+ +

The sequence side

+ +

Each voice sequence's subtrack sits in a spin loop:

+ +
L_0019: delay 1 frame
+        macro_read_port0
+        branch_rel_not_eq_zero L_0019   ; wait until Sou_VoiceStart writes the port
+        set_dyntbl D_0026
+        dyntbl_call                     ; index the table with the phoneme code
+        branch L_0019
+ +

The dynamic table has 128 entries pointing at 10-byte routines +C1 <instrument> / 88 <note> / FF. Entry v selects instrument +v − 1. Entry 0 is an immediate FF — nothing.

+ +

Per-frame parameter push

+ +

Sou_VoiceMake (game64.c_inc:1174) runs for both subtracks once +per game frame, inside Na_GameFrame:

+ +
volumeScale    = _08 * _0C;      // spec base × emotion volume
+frequencyScale = _14 * _18;      // spec base × emotion pitch
+NA_COMMAND_AUDIO_SUBTRACK_SET_VOL_SCALE (VOICE_GROUP, index, volumeScale);
+NA_COMMAND_AUDIO_SUBTRACK_SET_FREQ_SCALE(VOICE_GROUP, index, frequencyScale);
+NA_COMMAND_AUDIO_SUBTRACK_SET_PAN       (VOICE_GROUP, index, pan);
+NA_COMMAND_AUDIO_SUBTRACK_REVERB_VOLUME (VOICE_GROUP, index, reverbVolume);
+ +

pan is initialised to 0x3F (centre) and reverbVolume to 0, +and neither is ever written for voice subtracks. Animalese is always centred, dry, +and unaffected by the speaker's distance from the camera — unlike every other +positional SE in the game.

+ +

Because both subtracks share sou_voice_se[0] and [1] which are +always written together, an in-flight syllable's volume and pitch are retroactively +changed by the next syllable's settings.

+ +

Frame ordering

+ +

graph.c runs game_main (including the draw pass that calls +Na_VoiceSe) and then sAdo_GameFrame. So within one frame:

+ +
game_main   → mFontSentence_animal_voice_se → Na_VoiceSe  (sets params, writes port)
+Na_GameFrame → Sou_VoiceMake       (pushes params to the DSP)
+             → Sou_SpecialRoutine10 (fires any queued second syllable)
+ +

Sou_VoiceMake runs before Sou_SpecialRoutine10, so a +deferred syllable's parameters land one frame late.

+ +

Banks

+ +

From src/static/jaudio_NES/game/audioheaders.c:

+ + + + + + +
BankROM addrSizeparam0param1Instruments
1560x0005C2600x45000x05FF0x7700119
1570x000607600x30200x05FF0x530083
1580x000637800x45000x05FF0x7700119
+ +

param1 >> 8 is the instrument count, param0 >> 8 the sample +bank id (5 for all three), and all have zero drums and zero SFX. 119 instruments = +phoneme codes 0x01–0x77 exactly.

+ +

Bank 157 stopping at 83 is not an omission: instruments 0–82 cover codes +0x01–0x53, and the phonology engine never produces a code above +0x52 apart from 0x77. The missing range +0x53–0x76 is the editor's letter-name block, +which spec 3 can never reach. The only thing spec-3 voices genuinely lack is +you.

+ +

Sample details are in ../extraction.md: N64 VADPCM, tuning 0.6891 +(22050 Hz), order 2, two predictors, no loop, ~190 ms each.

+ +
+ ← 4. The phonology engine + 6. The soundfont lookup → +
+ + + diff --git a/docs/quirks.html b/docs/quirks.html new file mode 100644 index 0000000..bb860c9 --- /dev/null +++ b/docs/quirks.html @@ -0,0 +1,157 @@ + + + + + +8. Quirks, dead code & corrections — Animalese + + + + + + +

8. Quirks, dead code & corrections

+

Things a faithful port has to reproduce, things a clean reimplementation +should drop, and where earlier notes were wrong.

+ +

Bugs that shipped

+ +

Glyph scale → volume is a dead store

+
if (scale > 0x20) {
+    scale = scale - 0x20;
+    f31 = scale;
+    f31 = f31 * 0.15f;
+    f30 = sou_voice_se[0]._08 + f31;   // computed…
+    f30 = 4.f;                          // …and immediately discarded
+    sou_voice_se[0]._08 = f30;
+} else {
+    scale = 0x1f - scale;
+    f31 = scale * 0.02f;
+    f30 = sou_voice_se[0]._08 - f31;    // computed…
+    f30 = 0.4f;                         // …and immediately discarded
+    sou_voice_se[0]._08 = f30;
+}
+

game64.c_inc:2994

+

The intent was clearly a smooth scale-to-volume ramp. What ships is a step function: +any glyph larger than normal slams volume to 4.0 (a 6× jump over the +0.65 baseline), anything smaller to 0.4. Normal-size text passes +scale == 32 and skips the block entirely, so this is only audible on +deliberately scaled dialogue.

+

This is in the matching decomp, so retail does it. A faithful port needs it; a clean +library does not.

+ +

The mode-2 dedup check is tautological

+
case 2: {
+    if (sou_num_org == sou_num_org && sou_num_org == sou_last_num2) i = 1;
+}
+

game64.c_inc:2711

+

The first comparison is always true. The intent was presumably a three-in-a-row check +mirroring mode 0; the effect is a two-in-a-row check. Deferred syllables therefore +trigger the random substitution more often than intended.

+ +

SHIIN2BOIN is dead, and would read out of bounds

+
const u8 SHIIN2BOIN[] = { 0, 1, 2, 3, 4, 5, 1, 2, … };   // 86 entries
+…
+sou_now_boin = SHIIN2BOIN[a];    // a can be up to 0x77 = 119
+

game64.c_inc:236, written at :2863

+

sou_now_boin is written here and initialised in Sou_InitAudio, and +never read anywhere in the tree. Same for sou_nobasu_count +("elongation count"), which is only ever set to 0. Both are vestigial mouth-shape / +vowel tracking, presumably live in the N64 Dōbutsu no Mori code this was ported +from. Because the table is 86 entries and a can reach 119, the write reads +past the array — harmless only because the result is discarded.

+

The table also has an obvious data oddity: entry 45 is the value 45, where +every neighbour is 0–5.

+ +

Design choices that surprise

+ + + +

Corrections to earlier notes

+ +

The following supersede statements in ../extraction.md as originally written.

+ + + + + + + + + + + + + + + + + + + + + + + + +
WasActually
"Bank 157 has only 83 instruments … that voice collapses every digraph sample +above 0x53 onto one sound."Nothing above 0x53 is a digraph. The phonology engine's maximum output is +0x52, plus 0x77. Bank 157 covers the phonology range exactly; the +missing block is the editor's letter names, unreachable at spec 3. +The only real loss is you.
"43 of the 119 codes are unreachable through normal text … only ever hit by the +anti-repetition random substitution."36 of them (0x53–0x76) are reachable — through the mode-1 +editor path — and the random substitution can not reach them, since it only +produces 0x00–0x3F.
"Sou_SpecialRoutine10 replays it on the next audio frame."It runs inside Na_GameFrame, which graph.c calls once per +game frame. Same ~17 ms at 60 fps, but the mechanism is the game loop, and it +halves under SetGameFrame(2).
"♪ … calls Na_MelodyVoice(npc_id) — the villager's tune."It plays a fixed 16-note pattern {13, 14 × 15}, not a tune. The per-NPC +part is which instrument: npc_voice_id selects a chunk of sequence +248 to copy in. It is the speaker's timbre playing a constant figure.
Specs 5 and 6 listed only in the spec→sequence table, unattributed.They are the player's own voice in text-entry menus — 5 male, 6 +female — set from m_submenu.c:364, and are special-cased throughout +Na_VoiceSe.
Voice mode not covered.Three modes gate the whole pipeline, and the gate depends on the message, not just +the config. See gating.
Mode f == 1 not covered.A third calling convention that bypasses phonology entirely. See +the editor path.
+ +

Everything else in extraction.md §1 was re-verified against the current tree +and holds: the voice table, the 55 digraph entries, both vowel tables, the per-spec +timbre values, and the spec→sequence→bank mapping.

+ +
+ ← 7. The editor path + 9. Reference tables → +
+ + + diff --git a/docs/reference.html b/docs/reference.html new file mode 100644 index 0000000..5b86ebb --- /dev/null +++ b/docs/reference.html @@ -0,0 +1,217 @@ + + + + + +9. Reference tables — Animalese + + + + + + +

9. Reference tables

+

Everything needed to reimplement the pipeline, plus an index into the +decomp.

+ +

Voice codes

+ + + + + + + +
CodeCharClass
0x53–0x5C0–92 (neither)
0x5D 0x61 0x65 0x6A 0x6B 0x71a e i n o u0 (vowel)
other of 0x5D–0x76b c d f g h j k l m p q r s t v w x y z1 (consonant)
0x80–0x86, 0xFF♪  ␣  ?  !  ⏎  mute  click  empty2 (neither)
+ +

Letter L ↔ code: 0x5D + (L − 'a'). Digit D ↔ code: +0x53 + D.

+ +

Phoneme codes

+ +

Phoneme code v plays soundfont instrument v − 1. Code 0 is +silence. Codes marked † are produced by more than one rule and therefore share a +sample.

+ +

Short vowels (Sou_TanboinHenkan)

+ + + +
aiueon
0x010x140x290x0A0x1D0x1B
+ +

Long vowels (Sou_ChouboinHenkan)

+ + + +
aiueon
0x02 †0x15 †0x2A †0x0E †0x1E0x1B
+

Used only when class(a) == vowel && class(b) == consonant && c == 'e'. +The tables are otherwise byte-identical.

+ +

Consonants (both tables)

+ + + + + + + + + + + + + + + + + + + + + +
LetterCodeNotes
b0x07
c0x23 / 0x18soft (= s) if next letter is i or e, else hard (= k)
d0x09
f0x11
g0x12
h0x00silent — no instrument
j0x17shares ge
k q0x18
l0x19
m0x1A
p0x21
r0x22
s0x23
t0x26shares digit 2
v0x2D
w0x2E
x0x31shares ks
y0x15shares long i and ea
z0x30
+ +

Digits (both tables)

+ + + +
0123456789
0x320x330x260x270x340x350x360x370x100x38
+ +

Digraphs (Sou_ConnectCheck) — 53 entries

+ + + + + + + + + + + + + + + + + +
PairCodePairCodePairCodePairCode
ai0x0Fgo0x3Eno0x49si0x36
al0x02ha0x3Fof0x4Aso0x4F
an0x39he0x40oh0x4Bsu0x25
ar0x16hi0x41ok0x4Cth0x28
as0x3Aho0x42on0x4Dto0x50
be0x3Bi␣0x05oo0x2Aup0x51
by0x3Cif0x43or0x2Cwe0x52
ch0x08in0x44ou0x20wo0x33
do0x3Dir0x0Cow0x06you0x77
ea0x15is0x45oy0x1Fze0x32
er0x0Dit0x46re0x0E
fa0x35ks0x31se0x37
fo0x34me0x47sh0x24
ge0x17my0x48na0x38
ng0x1C
+

Of the 119 codes 0x01–0x77, the phonology engine produces 76. +The 43 it never produces are seven isolated gaps — +0x03 0x04 0x0B 0x13 0x2B 0x2F 0x4E — plus the 36-code block +0x53–0x76. The anti-repetition substitution can reach the first six +of the gaps (it only emits 0x00–0x3F); the block is reachable only +via the editor path; 0x4E appears to be reachable by +nothing at all.

+ +

Specs

+ + + + + + + + + + + +
SpecSeqBankVolPitchSpeaker
22431560.651.00lazy & jock villagers; most special NPCs (default)
32441570.651.00cranky villagers; Wendell, Pete, Booker, Jack
42451580.651.00normal, peppy, snooty villagers; Pelly, Phyllis, Mabel, Sable, Blanca; event narration
52431560.901.00player, male — text editor only
62451580.901.00player, female — text editor only
72431560.901.30Timmy & Tommy (via control code 93)
82451580.900.75Joan
92431560.700.65Farley
+ +

Text control codes that affect voice

+ + + + + + + + + + +
ValueNameEffect
3CURSOR_SET_TIMEpauses the cursor; lets a half-built window fire early
75–79, 97SET_MESSAGE_CONTENTS_*emotion state (normal / angry / sad / fun / sleepy / gloomy)
81SOUND_CUTplays system SE 0x54, sends the end code, suppresses voice
84SET_CHAR_SCALEchanges glyph scale → the volume step
90SET_LINE_SCALEsame, per line
92VOICE_TRUErestore spec (Timmy/Tommy only)
93VOICE_FALSEspec 7 (Timmy/Tommy only)
+ +

Source index

+ +

Paths relative to the ac-decomp root. Line numbers are for commit +09ca8e8b.

+ +

Message layer

+ + + + + + + + + + + + + + + + + + + + + +
src/game/m_msg_sound.c_inc:1mMsg_sound_voice_get — the 256-entry table
​:26mMsg_sound_voice_get_for_editor
​:44mMsg_sound_voice_get2 — newline injection
​:52mMsg_check_sound_special — forced-voice message ranges
​:66mMsg_check_sound_animal
​:85mMsg_sound_voice_entry — the (a, b, c) window
​:117mMsg_sound_voice_endcode_set
​:126mMsg_sound_CodeVoice
​:259mMsg_sound_voice_mode_get
​:288mMsg_sound_spec_change_voice_force
​:317mMsg_sound_spec_change_true/false — Timmy/Tommy
src/game/m_msg_cursol.c_inc:1100mMsg_Main_Cursol_ControlCursol — the cursor loop
​:1184the NOT_PAUSE_FRAME toggle
src/game/m_msg_draw_font.c_inc:35voice-fire decision
src/game/m_font_main.c_inc:951mFontSentence_animal_voice_se — scale, and the call
​:1078the draw-loop trigger
src/game/m_npc.c:32l_sp_actor_name — special-NPC spec table
​:4116mNpc_GetNpcSoundSpec
src/game/m_submenu.c:364player voice, specs 5 / 6
src/game/m_editor_ovl.c:712the mode-1 editor path
+ +

Bridge

+ + + + + + +
src/audio.c:96sAdo_VoiceSe
​:152sAdo_SetVoiceMode
​:216sAdo_SpecChange
include/audio.h:45VOICE_STATUS_*
include/audio_defs.h:10VOICE_MODE_*
+ +

Audio engine — src/static/jaudio_NES/game/game64.c_inc

+ + + + + + + + + + + + + + + + + + + + +
:22VOICE_GROUP = NA_GROUP4
:97SOU_VOICE_SE struct
:236SHIIN2BOIN (dead)
:311sou_voice_se_init — pan 0x3F, reverb 0
:820Sou_VoiceStart
:1174Sou_VoiceMake
:1708Sou_SpecialRoutine10 — deferred syllable
:1852Na_GameFrame
:2146Sou_TanboinHenkan
:2231Sou_ChouboinHenkan
:2316Sou_ConnectCheck
:2564Sou_BoinShiinCheck
:2598Na_VoiceSe
:3319Na_SetVoiceMode
:3459Na_SpecChange
melody.c:727Na_MelodyVoice — the ♪ handler
audioheaders.c:4073bank 156–158 entries
audioheaders.c:4176AudiodataHeaderStart — the three archives
audioheaders.c:4717seq 243–245 → bank 156–158
+ +

Soundfont / bank loading

+ + + + + + + + + + + + + +
include/jaudio_NES/audiostruct.h:74ArcEntry / ArcHeader
​:95adpcmloop, adpcmbook, smzwavetable, wtstr
​:348voicetable — the instrument
​:373voiceinfo — per-bank runtime info
src/static/jaudio_NES/internal/system.c:772__Link_BankNum — alias entries
​:813Nas_BankOfsToAddr_Inner — offset → pointer
​:1171__SetVlute — param0/1/2 decode
​:1692__WaveTouch — sample vs bank relocation
internal/channel.c:354NoteToVoice
​:368ProgToVp — instrument index → voicetable
internal/track.c:1159Nas_ProgramChanger
​:1399SUBTRACK_CMD_SET_INSTRUMENT_BANK — reverse bank-list index
+ +
+ ← 8. Quirks & corrections + Index +
+ + + diff --git a/docs/scheduling.html b/docs/scheduling.html new file mode 100644 index 0000000..581dff6 --- /dev/null +++ b/docs/scheduling.html @@ -0,0 +1,147 @@ + + + + + +3. Windowing & timing — Animalese + + + + + + +

3. Windowing & timing

+

The phonology engine needs three characters at once. Getting them there +takes two cursor steps, a pair of status flags, and a trigger that fires from the font +renderer rather than the message logic.

+ +

The text cursor

+ +

mMsg_Main_Cursol_ControlCursol (src/game/m_msg_cursol.c_inc:1100) +runs once per game frame while a message is scrolling. Its loop:

+ +
    +
  1. Decrement cursor_timer; if still positive, do nothing this frame. + (Control code mFont_CONT_CODE_CURSOR_SET_TIME, value 3, sets it — this is + how messages pause mid-sentence.)
  2. +
  3. Skip newlines, message tags and control codes, executing their side effects.
  4. +
  5. On a printable character: advance end_text_cursor_idx + only if mMsg_STATUS_FLAG_NOT_PAUSE_FRAME is clear, + then toggle that flag, then call mMsg_sound_CodeVoice(msg_p, cur_idx) + and break.
  6. +
+ +

The flag toggle is the speed control: the index moves on alternate frames, so text +advances at one character per two frames, roughly 33 characters/second +at 60 fps.

+ +

Holding B sets mMsg_STATUS_FLAG_FAST_TEXT, which zeroes +cursor_timer and clears NOT_PAUSE_FRAME each frame, giving one +character per frame. It also switches the reported message speed from 2 to 1 +(mMsg_sound_MessageSpeedClear), which the audio engine stores in +sou_message_speed.

+ +

Building the (a, b, c) window

+ +

mMsg_sound_CodeVoice (m_msg_sound.c_inc:126) converts the character +and hands it to mMsg_sound_voice_entry (:85), which is the state +machine:

+ +
if (idx == msg_p->voice_sfx_idx) return;          // already seen this index
+if (idx == msg_p->end_text_cursor_idx) return;    // this is a non-advancing frame
+msg_p->voice_sfx_idx = idx;
+
+if (status_flags & VOICE_ENTRY) {
+    // second character of the pair
+    voice2_idx = voice_no;                        // b
+    voice3_idx = lookahead(idx + code_size);      // c, or 0xFF at a newline
+    status_flags |= FLAG_18;
+}
+else if (voice_no != 0x81) {                      // a space cannot open a pair
+    voice_idx  = voice_no;                        // a
+    voice2_idx = voice3_idx = 0xFF;
+    animal_voice_idx = npc_voice_id;
+    status_flags |= VOICE_ENTRY;
+}
+ +

Two guards deserve attention. The end_text_cursor_idx comparison is what +makes the every-other-frame toggle actually halve the entry rate — on a +non-advancing frame the pre-advance index equals the post-advance index and the call +is dropped. And a space (0x81) is never allowed to be the a of a +window, only the b; this is why utterances tend to break after a +space rather than starting on one.

+ +

The c lookahead reads one character past b and clamps a newline or a +control code to 0xFF. It exists for exactly two rules in the phonology: +you and magic e.

+ +

Firing

+ +

Nothing plays from the message code. The trigger is in the draw pass — +mMsg_draw_font (src/game/m_msg_draw_font.c_inc:35) decides whether +this frame's glyph should speak:

+ +
if (status_flags & VOICE_ENTRY) {
+    if (status_flags & FLAG_18)                  voice = TRUE;   // pair complete
+    else if (cursor_timer != 0 || force_voice_enable_flag) voice = TRUE;
+}
+ +

It then finds the line containing voice_sfx_idx, sets +mFont_SENTENCE_FLAG_VOICE_SE on that line's mFontSentence, and clears +both flags. mFontSentence_gppDraw_main +(src/game/m_font_main.c_inc:1078) calls the speak function immediately after +drawing the glyph at index se_pos, so audio is frame-locked to the character +appearing on screen.

+ +

The second branch is the escape hatch: if the cursor is paused (a +CURSOR_SET_TIME code) or a control code returned +mMsg_RESULT_TRUE, a half-built window with b = c = 0xFF fires anyway +rather than being held indefinitely.

+ +

Scale → volume

+ +

mFontSentence_animal_voice_se (m_font_main.c_inc:951) computes the +scale argument from the glyph's own transform:

+ +
f_scale = sqrtf(scale.x² + scale.y²);
+scale   = line_scale * f_scale * (32.0f / √2) + 0.5f;   // clamped 0…255
+ +

32 is normal size, so a normally scaled glyph passes scale == 32 and the +volume adjustment is skipped entirely. (What happens for other values is +not what it looks like.)

+ +

It also contains one very specific special case: if the character is ♪ at exactly +normal scale in colour rgba(50, 60, 50, 255), the NPC id is forced to 0, +suppressing the melody. This is presumably a specific piece of dialogue where a +literal music note glyph should stay silent.

+ +

End of message

+ +

mMsg_sound_voice_endcode_set (m_msg_sound.c_inc:117) sends +sAdo_VoiceSe(0x84, 0xFF, 0xFF, npc, 32, 0) — a synthetic newline — and clears +both window flags. It runs when the message ends, and also when the +mFont_CONT_CODE_SOUND_CUT control code (value 81) appears, which additionally +plays system SE 0x54 and suppresses the rest of the line's voice.

+ +

The resulting cadence

+ +
frame  0   char A   entry: a = A                     (nothing plays)
+frame  1   —        non-advancing frame, dropped
+frame  2   char B   entry: b = B, c = lookahead(C)
+                    draw:  sAdo_VoiceSe(A, B, C, …)   ← one call
+frame  3   —        Sou_SpecialRoutine10 replays B as mode 2
+frame  4   char C   entry: a = C
+…
+ +

So: one Na_VoiceSe call per two characters, one pair every four frames +(~66 ms), with the deferred second syllable one game frame (~17 ms) behind the first. +Samples run about 190 ms, so syllables overlap heavily — which is why there are +two alternating subtracks.

+ +
+ ← 2. Text → voice code + 4. The phonology engine → +
+ + + 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 → +
+ + + diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000..78917cf --- /dev/null +++ b/docs/style.css @@ -0,0 +1,83 @@ +/* minimal shared styling */ +:root { + --fg: #1a1a1a; + --dim: #666; + --rule: #d8d8d8; + --bg: #fdfdfc; + --code-bg: #f3f3f1; + --accent: #7a4a12; +} +@media (prefers-color-scheme: dark) { + :root { + --fg: #e4e2de; + --dim: #999; + --rule: #3a3a3a; + --bg: #17181a; + --code-bg: #202225; + --accent: #d8a76a; + } +} + +html { -webkit-text-size-adjust: 100%; } +body { + max-width: 46rem; + margin: 0 auto; + padding: 2.5rem 1.25rem 6rem; + background: var(--bg); + color: var(--fg); + font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; +} + +h1, h2, h3 { line-height: 1.25; font-weight: 600; } +h1 { font-size: 1.65rem; margin: 0 0 .25rem; } +h2 { font-size: 1.2rem; margin: 2.5rem 0 .75rem; padding-top: .75rem; border-top: 1px solid var(--rule); } +h3 { font-size: 1rem; margin: 1.75rem 0 .5rem; } + +a { color: var(--accent); } +a:hover { text-decoration: none; } + +nav.crumb { font-size: .85rem; color: var(--dim); margin-bottom: 2rem; } +nav.crumb a { color: var(--dim); } + +p.lede { color: var(--dim); margin: 0 0 2rem; } + +code, kbd { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .875em; } +:not(pre) > code { background: var(--code-bg); padding: .1em .35em; border-radius: 3px; } + +pre { + background: var(--code-bg); + padding: .85rem 1rem; + border-radius: 4px; + overflow-x: auto; + font-size: .8125rem; + line-height: 1.5; +} + +table { border-collapse: collapse; width: 100%; margin: 1rem 0; font-size: .875rem; display: block; overflow-x: auto; } +th, td { text-align: left; padding: .35rem .7rem .35rem 0; border-bottom: 1px solid var(--rule); vertical-align: top; } +th { font-weight: 600; white-space: nowrap; } +td code { white-space: nowrap; } + +ul, ol { padding-left: 1.3rem; } +li { margin: .3rem 0; } + +.src { font-size: .8125rem; color: var(--dim); } +.src code { background: none; padding: 0; } + +.note { + border-left: 3px solid var(--accent); + padding: .1rem 0 .1rem .9rem; + margin: 1.25rem 0; + color: var(--dim); +} +.note strong { color: var(--fg); } + +.toc { list-style: none; padding: 0; } +.toc li { margin: .9rem 0; } +.toc a { font-weight: 600; } +.toc span { display: block; color: var(--dim); font-size: .9rem; font-weight: 400; } + +.pager { margin-top: 3.5rem; padding-top: 1rem; border-top: 1px solid var(--rule); font-size: .9rem; display: flex; justify-content: space-between; gap: 1rem; } + +.grid2 { columns: 2; column-gap: 2rem; font-size: .875rem; } +@media (max-width: 40rem) { .grid2 { columns: 1; } } diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..fa9e125 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1788316716, + "narHash": "sha256-bc7rSpXIdn9QWGNqfWcPZWOhEVF8NoeAZkWq0XWnf/k=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "3ed67ec0a4d3c7ab4ae1f04f8ee8df07bfa506a2", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c3900bb --- /dev/null +++ b/flake.nix @@ -0,0 +1,41 @@ +{ + description = "animalese - Animal Crossing style speech synthesis"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = inputs: { + packages = builtins.mapAttrs (system: pkgs: { + animalese = pkgs.rustPlatform.buildRustPackage { + pname = "animalese"; + version = "0.1.0"; + + src = inputs.self; + cargoLock.lockFile = ./Cargo.lock; + + meta = { + description = "Animal Crossing style speech synthesis"; + mainProgram = "animalese"; + }; + }; + + default = inputs.self.packages.${system}.animalese; + }) inputs.nixpkgs.legacyPackages; + + devShells = builtins.mapAttrs (system: pkgs: { + default = pkgs.mkShell { + packages = [ + pkgs.cargo + pkgs.rustc + pkgs.clippy + pkgs.rustfmt + pkgs.rust-analyzer + ]; + + # so rust-analyzer can find the standard library sources + RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; + }; + }) inputs.nixpkgs.legacyPackages; + }; +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d35753c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,20 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + #[arg(short, long)] + volume: f64, + + #[arg(short, long)] + timbre: f64, +} + +mod tokenizer; +use tokenizer::VoiceCode; + +fn main() { + let _args = Args::parse(); + + println!("Hello, world!"); +} diff --git a/src/phonemes/mod.rs b/src/phonemes/mod.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/phonemes/phonemes.rs b/src/phonemes/phonemes.rs new file mode 100644 index 0000000..9313c21 --- /dev/null +++ b/src/phonemes/phonemes.rs @@ -0,0 +1,6 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum Phoneme { + +} + + diff --git a/src/tokenizer/mod.rs b/src/tokenizer/mod.rs new file mode 100644 index 0000000..54ea9a3 --- /dev/null +++ b/src/tokenizer/mod.rs @@ -0,0 +1,2 @@ +mod tokens; +pub use tokens::VoiceCode; diff --git a/src/tokenizer/tokens.rs b/src/tokenizer/tokens.rs new file mode 100644 index 0000000..fa1d7e7 --- /dev/null +++ b/src/tokenizer/tokens.rs @@ -0,0 +1,74 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum ControlCode { + Melody, // 0x80 + Space, // 0x81 + Question, // 0x82 + Bang, // 0x83 + Newline, // 0x84 + Punct, // 0x85 + Bebe, // 0x86, + Control, // -1 +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Letter { + A, // 0x5D + B, // 0x5E + C, // 0x5F + D, // 0x60 + E, // 0x61 + F, // 0x62 + G, // 0x63 + H, // 0x64 + I, // 0x65 + J, // 0x66 + K, // 0x67 + L, // 0x68 + M, // 0x69 + N, // 0x6A + O, // 0x6B + P, // 0x6C + Q, // 0x6D + R, // 0x6E + S, // 0x6F + T, // 0x70 + U, // 0x71 + V, // 0x72 + W, // 0x73 + X, // 0x74 + Y, // 0x75 + Z, // 0x76 +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Digit { + D0, // 0x53 + D1, // 0x54 + D2, // 0x55 + D3, // 0x56 + D4, // 0x57 + D5, // 0x58 + D6, // 0x59 + D7, // 0x5A + D8, // 0x5B + D9, // 0x5C +} + +#[derive(Debug)] +pub enum VoiceCode { + Letter(Letter), + ControlCode(ControlCode), + Digit(Digit) +} + + +impl Letter { + fn is_shiin(&self) -> bool { + !self.is_boin() + } + + fn is_boin(&self) -> bool { + use Letter::*; + [A, E, I, O, U, N].contains(&self) + } +} -- cgit v1.3.1