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];

The full 256-byte array is reproduced verbatim in the reference tables; this page describes its shape.

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 →