Voice & Category Resolution¶
A single PlayVoice(actor, "Category") call runs two lookups in order:
- Slot resolution — which voice pack does this actor use?
- Category resolution — which folder inside that slot does
"Category"map to?
Understanding these two chains is the whole game when authoring an AudioUtil.toml.
1. Slot resolution — the actor's voice¶
Checked top to bottom; first hit wins.
| # | Source | Notes |
|---|---|---|
| 1 | pc_female_slot / pc_male_slot |
Player only. The PC always resolves here, and no other actor ever does. Empty = no reservation. |
| 2 | [npc_overrides] |
Explicit per-NPC pin, 'Plugin.esp\|FormID' = "Slot". May target any slot, including a pc-reserved one. |
| 3 | [voicetype_remap] → [voicetype_map] |
Rename the actor's voicetype (one hop) to one you have, then map that voicetype to a slot. |
| 4 | [race_map] |
Substring match against the race editor id; most specific (longest) hint wins. Creatures resolve here. |
| 5 | default_creature_slot |
Creatures only, and they stop here — step 6 is never reached. Empty (the default) = silent. |
| 6 | default_female_slot / default_male_slot |
Non-creatures: last resort, by the actor's sex. If that slot is missing or pc-reserved, the first free slot of the actor's sex. |
GetSlotForActor(actor) returns exactly what this chain produces ("" if nothing resolves). Non-player actors never resolve to a pc-reserved slot.
Creatures branch at step 5¶
A creature — an actor whose race carries no ActorTypeNPC keyword — is routed by steps 1–4 like anyone else, so a [race_map] hint onto a sex = "all" slot is the normal way to voice one. What it may not do is fall through to the by-sex default: every slot that step can reach is an 'F'/'M' human voice pack, so an unmapped frostbite spider or fox would end up speaking human lines from whichever male pack happened to be declared first.
So creatures take default_creature_slot instead, and stop there whether or not it resolves:
[general]
default_creature_slot = "" # unrouted creatures stay silent (the default)
# default_creature_slot = "C0" # ...or give them all one generic growl slot
Silent is the useful default: GetSlotForActor returns "", so a script can test whether a creature is voiced before building a line around it.
In vanilla, ActorTypeNPC sits on only the playable humanoid races — the ten player races plus their vampire and child variants, ElderRace, DremoraRace, DA13AfflictedRace and a few scripted stand-ins. Everything else is a creature by this test, draugr, falmer and werewolves included. That's deliberate: humanoid-sounding creatures are exactly the ones you want a [race_map] hint for, and a hint resolves at step 4, so they never reach the creature branch at all.
Spreading across a slot list¶
[voicetype_map] and [race_map] values may be a single slot or a list of candidates:
With a list, actors sharing the voicetype are spread across the slots deterministically by form id — the same NPC always resolves to the same slot, every scene, load-order independent.
[voicetype_remap] — covering voicetypes you lack¶
Renames a voicetype to one you do have a pack for, before the slot lookup. Values are voicetype names (a single hop — the target must be a [voicetype_map] key), not slot ids:
[voicetype_remap]
enable = true
MaleGuard = "MaleNord" # a guard now resolves as a Nord
MaleCommander = "MaleBrute"
enable = false turns the whole layer off, so only exact [voicetype_map] matches get voices.
[race_map] — race and creatures¶
Hints are substring-matched against the race editor id, longest hint first:
[race_map]
Nord = "M4" # matches NordRace and NordRaceVampire
Troll = "C1" # matches TrollRace and TrollFrostRace
The longest-match rule is what keeps Werewolf and Wolf, or Dog and a custom husky race, apart. Creature races usually have no ActorBase sex and read as male.
[npc_overrides] — pinning one NPC¶
The key is 'Plugin.esp|FormID'. For ESL-flagged plugins use the last 3 hex digits of the form id ('MyEslMod.esp|D62'). Checked before voicetype/race resolution; may target any slot, including a reserved one. This is how you give a specific follower her own voice.
gag_slot — muffled voice when gagged¶
After the slot is resolved by the steps above, one more check runs: if the actor wears a gag device (a worn keyword or item configured in [gag]) and the resolved slot names a gag_slot, resolution switches to that gag slot. Category resolution (section 2) then runs there instead — same category name, muffled audio.
If the requested category has no audio in the gag slot, [gag] default_category plays there as a catch-all, so a gagged actor never leaks a clear line and never falls silent. Give the gag slot no clear fallback if you want strictly-gagged output — otherwise its own fallback chain could resolve the category to clear audio before the catch-all is reached.
Lipsync is suppressed for the gagged actor at the same time (the device owns the mouth).
2. Category resolution — the folder inside the slot¶
Once the slot is known, "Category" resolves against that slot, in this order:
exact folder
→ [category_aliases] (rename: script name -> on-disk folder)
→ [male_only_remap] (male slots only)
→ [category_fallbacks] (substitute; chained - see below)
→ the slot's `fallback` slot (retry the whole chain there)
→ [sfx] table (last resort, same name)
If nothing resolves, PlayVoice returns 0 and logs it.
[category_aliases] — script name vs. folder name¶
When your Papyrus category name doesn't match the on-disk folder name, alias it (split by sex):
[category_aliases.female]
BattleCry = "War Shout" # PlayVoice(...,"BattleCry") plays the "War Shout" folder
[category_aliases.male]
# ...
[male_only_remap] — female-engine categories on a male voice¶
When a male slot receives a category named for the female script set (male-only scenes route female-engine decisions to male voices), substitute the closest male category. Male slots only:
[category_fallbacks] — substitute an empty category¶
Chained (since 0.9.18, max 8 hops, cycles broken): if the substitute has no folder either, its own fallback entry is tried, and so on. This lets a table be authored as a ladder from a specific category down to a general one — each rung a real folder some pack ships — so a pack is served by the nearest rung it has:
Note the whole ladder is walked inside one slot before the slot's fallback
slot is consulted, so a deeper rung in the pack wins over a shallower rung in the
stock slot. That is the same preference [category_aliases] already encodes (a
pack's own audio over stock), but it does mean adding a rung can move where an
existing request lands. Split by sex:
fallback — backfill from another slot¶
A [[slot]] may name a fallback slot. Any category that resolves to nothing in this slot (no folder, alias, or category fallback) is retried in the fallback slot — per category, running the whole category chain there. Chains are allowed, capped at 4 hops.
[[slot]]
id = "F1"
sex = "female"
path = 'Sound\fx\MyMod\F1'
fallback = "F0" # categories F1 lacks fall through to F0
This is how a scanned pack slot backfills from a stock slot: an empty pack folder means every category falls through; a partial pack, only the ones it lacks. It lets a player drop a partial voice pack into F1 and still have every category covered.
[sfx] last resort¶
If a category still has no folder after everything above, PlayVoice tries the [sfx] table under the same name before giving up — so a body-SFX name can double as a voice category when it makes sense.
Debugging resolution¶
GetSlotForActor(actor)→ the slot the chain picks (""= none).CategoryExists(slot, category)→ does category resolution find at least one file?GetCategoryFileCount(slot, category)→ how many files, after aliases/fallbacks.ReloadConfig()→ re-read the TOML and rescan folders live, then re-test.