Skip to content

AudioUtil (core player)

Scriptname AudioUtil Hidden — the native folder-based audio player. Read Overview & Concepts first for the handle / volume / group / channel model that every Play* call shares.

All name and key matching (categories, slots, groups, SFX names) is case- and space-insensitive: "Battle Cry" == "BattleCry" == "battlecry".

Core

GetAPIVersion

int Function GetAPIVersion() global native

API version of the loaded DLL, for compatibility checks. 0 = DLL not installed. Increases only when signatures/behavior change incompatibly. Currently 6 (v2 added GetSlotVariation, v3 GetResolvingSlot, v4 GetHandlePath, v5 captions, v6 the tag-scored natives).

ReloadConfig

bool Function ReloadConfig() global native

Re-read AudioUtil.toml and rescan all slot folders in-game — no restart needed. Returns false if the file failed to parse, in which case the previous config stays active. Console: au reload — see Console Commands.

Playback

PlayVoice

int Function PlayVoice(Actor akActor, string category, float volume = 1.0, string group = "", string channel = "", bool blockLipSync = false) global native

Play a voice line for an actor. Resolves which voice pack (slot) the actor uses, then which folder inside it the category names, then shuffle-picks a wav. Returns a handle; 0 if no slot or no audio resolved.

  • Slot resolution and category resolution are described in full under Voice & Category Resolution.
  • blockLipSync = true plays this one line without moving the speaker's mouth — a per-call opt-out. If your mod owns the actor's face across a whole scene, pass it true on every line while the face is up (see the tip under lipsync).
int h = AudioUtil.PlayVoice(npc, "BattleCry")
AudioUtil.PlayVoice(npc, "Taunt", 0.8, "npc_voice")            ; + volume, group
AudioUtil.PlayVoice(npc, "Taunt", 1.0, "npc_voice", "npc_main") ; channel: replaces prior line

PlayVoiceTagged

int Function PlayVoiceTagged(Actor akActor, string category, string tags, float volume = 1.0, string group = "", string channel = "", bool blockLipSync = false, bool blockCaption = false) global native

API v6+. PlayVoice plus a fact string ("angry intense undead" — whitespace/comma separated, case-insensitive, at most one fact per axis). Inside the resolved category, a tagged pool competes only if all its tags appear among the facts; the highest axis-weight sum wins and untagged files are the always-valid floor. Unknown fact tokens are ignored (warned once); empty tags — or no [tags] vocabulary configured — makes this identical to PlayVoice. blockCaption = true suppresses the line's caption sidecar — no HUD subtitle, no AudioUtil_Caption event — the caption analogue of blockLipSync, for a caller rendering its own text. See Tag-Scored Pools.

int h = AudioUtil.PlayVoiceTagged(npc, "BattleCry", "angry intense undead")

PlayVoiceFromSlot

int Function PlayVoiceFromSlot(string slot, string category, Actor akFollow, float volume = 1.0, string group = "", string channel = "", bool blockLipSync = false) global native

Same as PlayVoice, but the slot is named explicitly ("F1", "M4", "C2", …) instead of resolved from an actor — for samples, tests, or when the caller already decided the voice. akFollow only provides the 3D position.

PlayVoiceFromSlotTagged

int Function PlayVoiceFromSlotTagged(string slot, string category, string tags, Actor akFollow = None, float volume = 1.0, string group = "", string channel = "", bool blockLipSync = false, bool blockCaption = false) global native

API v6+. Tag-scored variant of PlayVoiceFromSlot — the fact string and blockCaption work exactly as in PlayVoiceTagged.

PlaySFX

int Function PlaySFX(string sfxName, Actor akFollow, float volume = 1.0, string group = "sfx", string channel = "") global native

Play a named SFX. sfxName resolves first as a category of the sfx slot (SFX0 by default; set via [general] sfx_slot), then the flat [sfx] table — so an sfx pool can use a scanned folder, an explicit file list (BSA-capable), or a folder ref, exactly like a voice category. See SFX resolution. Defaults into the sfx group so SFX volume/ducking applies unless you override it.

PlayFile

int Function PlayFile(string dataRelativePath, Actor akFollow, float volume = 1.0, string group = "", string channel = "") global native
int Function PlayFileWithLipSync(string dataRelativePath, Actor akFollow, float volume = 1.0, string group = "", string channel = "") global native

PlayFile never drives the mouth — it also serves non-vocal one-shots (squelches, impacts), and its behavior is unchanged for existing consumers. For a spoken line, use PlayFileWithLipSync (API v5+): identical arguments, plus akFollow's mouth is driven from the clip's loudness exactly like a PlayVoice line — honoring the global [lipsync] toggle and the automatic guards (gag, worn tongue, in-dialogue). Lipsync works for loose PCM wav and fuz (via the decoded cache); other formats play normally, mouth-still.

Play one specific file. Path is relative to Data\ ('Sound\fx\MyMod\a.wav') and may resolve to a loose file or to audio packed inside a BSA — the engine's resource loader handles both (loose wins over archive).

Supported formats: wav, xwm, and fuz. A .fuz voice container — e.g. any vanilla dialogue line, Sound\Voice\Skyrim.esm\MaleNord\…​.fuz, straight out of the voice BSAs — has its xWMA audio decoded once to PCM (via the OS's own WMA decoder, no bundled codecs) into Sound\AudioUtilFuzCache\ and plays from there on every later call (the cache persists across sessions and can be deleted freely — it rebuilds on demand). Because the cached copy is plain PCM wav, fuz lines get the full treatment: captions keyed off the .fuz path (foo.fuz + foo.toml sidecar) and amplitude lipsync, same as a loose wav. If the decode ever fails, the raw xwm payload is cached instead — the line still plays, just mouth-still.

PlayFolder

int Function PlayFolder(string dataRelativeFolder, Actor akFollow, float volume = 1.0, string group = "", string channel = "") global native
int Function PlayFolderWithLipSync(string dataRelativeFolder, Actor akFollow, float volume = 1.0, string group = "", string channel = "") global native

Play a random file from a loose folder (scanned on first use, then cached as a shuffle bag). Scans pick up .wav, .xwm, and .fuz files. PlayFolder never drives the mouth; PlayFolderWithLipSync (API v5+) is the spoken-line variant — the picked clip lipsyncs akFollow like a PlayVoice line, with the same global toggle and guards.

Folder scans can't see into BSAs

PlayFolder (and folder-string categories, and the [sfx] table) scan the loose filesystem only. For BSA-packed audio use PlayFile or an explicit [slot.categories] file list in the TOML — those go through the engine's resource loader, which resolves archives.

Handles

IsHandlePlaying

bool Function IsHandlePlaying(int handle) global native

True while the instance is audible. Also true during a short startup grace (~0.4 s) right after Play* returns: the engine starts streams asynchronously and briefly reports "not playing" for a sound that is about to start.

StopHandle

bool Function StopHandle(int handle) global native

Stop the instance now. Returns true if it was alive and stopped.

GetHandleDuration

float Function GetHandleDuration(int handle) global native

Clip length in seconds. May return 0.0 while the stream is still being prepared — see WaitForHandle for how it copes with that.

GetHandlePath

string Function GetHandlePath(int handle) global native

The data-relative path of the file this handle actually played — the exact wav the shuffle bag picked (e.g. Sound\SLOVE\F1\Moan\moan_03.wav). Works for every Play*. Returns the full Data-relative path with \ separators; "" for a dead/unknown handle. Read it right after the Play* call returns — the path is retained only while the instance is alive. Requires API version >= 4.

SetHandleVolume

Function SetHandleVolume(int handle, float volume) global native

Change one instance's base volume mid-play (group multipliers still apply).

Groups and channels

Conventional group names: pc_high / pc_low (player voice), partner_high / partner_low (NPC voice), sfx (effects), oneshot (ambient one-shots). Any other name works — groups are created on first use.

SetGroupVolume

Function SetGroupVolume(string group, float volume) global native

Set a group's volume (0.01.0). Applies immediately to playing members and to everything started later. A consumer mod calls this at runtime (e.g. from its own MCM — AudioUtil itself ships none) — the TOML [groups] values are only the startup state.

DuckGroup / UnduckGroup

Function DuckGroup(string group, float factor = 0.0) global native
Function UnduckGroup(string group) global native

DuckGroup temporarily scales a group by factor (0.0 = silent, 1.0 = no duck) without touching its stored volume — e.g. duck background voice while an important line plays. Ducks do not stack; the last factor wins. UnduckGroup restores the group to its normal volume (equivalent to factor 1.0).

StopGroup / StopAllAudio / StopChannel

Function StopGroup(string group) global native
Function StopAllAudio() global native
Function StopChannel(string channel) global native
  • StopGroup — stop every playing instance in the group.
  • StopAllAudio — stop everything AudioUtil is playing and clear all channel bindings. Does not touch group volumes or the config.
  • StopChannel — stop whatever occupies the channel (if anything) and free it.

Lipsync

PlayVoice / PlayVoiceFromSlot / PlayFileWithLipSync automatically move the speaking actor's mouth in sync with the clip's loudness: the DLL reads the wav's amplitude envelope and drives the MFG Aah / BigAah phonemes per frame. Works for loose PCM wav files and fuz (whose audio is decoded to PCM in the fuz cache, so even BSA-packed vanilla lines lipsync); raw xwm or BSA-packed wav plays normally but skips the mouth. Configure defaults in the TOML [lipsync] table.

Lipsync is also suppressed automatically for a gagged actor (one wearing a device configured in the TOML [gag] table) — the device owns the mouth, so the DLL won't fight it. It's likewise suppressed while an actor is in a dialogue with the player (the game's dialogue/voice system drives that mouth), toggleable via [lipsync] block_in_dialogue (default on). No Papyrus call needed for either.

Playing nice with expression mods

Only the two phonemes above are touched, and they are zeroed when the clip ends. If your mod sets phonemes itself (e.g. an expression cycler), skip your own mouth writes for an actor while IsLipSyncActive(actor) is true to avoid fighting over the jaw. If your mod fully owns the face (an overlay that sets its own expression), pass blockLipSync = true on the lines you play for that actor while the face is up so AudioUtil never touches its mouth (see the tip above).

IsLipSyncActive

bool Function IsLipSyncActive(Actor akActor) global native

True while AudioUtil is driving this actor's mouth (a voice line with a readable envelope is playing and lipsync is enabled).

StartLipSync / StopLipSync

Function StartLipSync(Actor akActor, int handle) global native
Function StopLipSync(Actor akActor) global native

StartLipSync opts an already-playing instance into lipsync — drives akActor's mouth from the clip's loudness like a PlayVoice line. PlayVoice/PlayVoiceFromSlot/PlayFileWithLipSync already lipsync on their own; this is for PlayFolder/PlayFile results or for re-attaching a mouth that was stopped. Loose PCM wav and fuz only; no-op on a dead/unknown handle. StopLipSync releases the actor's mouth; the audio itself keeps playing.

SetLipSyncEnabled / IsLipSyncEnabled

Function SetLipSyncEnabled(bool enable) global native
bool Function IsLipSyncEnabled() global native

Master switch (runtime; the TOML value is restored on ReloadConfig). Turning it off fades all active mouths closed.

SetLipSyncGain

Function SetLipSyncGain(float gain) global native

Mouth-open strength, 0.02.0 (1.0 = envelope as-is). For a consumer mod's runtime control (e.g. its MCM).

Owning an actor's face across many lines

There is no standing per-actor lipsync block. If your mod takes over an actor's face (an ahegao / expression overlay), pass blockLipSync = true on each PlayVoice for that actor while the face is up — decide it per call from your own face-ownership state. Because it's decided per line rather than latched, there's no state to leak or to be cleared on load, and two systems that both play lines for the same actor can't clobber each other's block. For a whole category that should never lipsync (oral SFX, climax pools), list it in [lipsync] block_categories instead.

Captions

On-screen captions for played lines (API version >= 5). When a wav has a same-named .toml sidecar next to it:

Sound\SLOVE\F1\Moan\moan_03.wav
Sound\SLOVE\F1\Moan\moan_03.toml
en = "Does this count as extra follower duty?"
ru = "Это считается дополнительной службой?"

then, while the line plays, the text for the configured [captions] language is shown as a regular game subtitle attributed to the speaking actor — bottom center, with the speaker's name, exactly like an overheard dialogue line — and removed when the line ends, is stopped, or is replaced on its channel. No sidecar = no caption; packs opt in per file. Sidecars are loose files only (not read out of BSAs, same constraint as lipsync).

Works for every Play* that has an actor to attribute the line to (PlayVoice / PlayVoiceFromSlot / PlaySFX / PlayFile / PlayFolder with a non-None actor).

A mod event is also sent for consumers that want their own presentation (with [captions] hud = false to suppress the built-in subtitle):

RegisterForModEvent("AudioUtil_Caption", "OnAudioUtilCaption")

Event OnAudioUtilCaption(string eventName, string text, float handle, Form sender)
    ; text = caption text, handle = the Play* instance handle, sender = the speaking actor
EndEvent

GetHandleCaption

string Function GetHandleCaption(int handle) global native

Caption text of the file this handle played, resolved for the current [captions] language ("" if the wav ships no sidecar or no usable key). Independent of enable/hud, so a mod can fetch the text and render it itself. Like GetHandlePath, read it right after the Play* call returns.

SetCaptionsEnabled / AreCaptionsEnabled

Function SetCaptionsEnabled(bool enable) global native
bool Function AreCaptionsEnabled() global native

Master switch (runtime; the TOML value is restored on ReloadConfig). Turning it off also clears any caption currently on screen.

Introspection

GetSlotForActor

string Function GetSlotForActor(Actor akActor) global native

Which slot id PlayVoice would resolve for this actor right now ("M4", "F1", …; "" if none). Useful for debugging voice assignment.

GetSlotVariation

string Function GetSlotVariation(string slot) global native

The optional variation schema label a slot declares: "B" for a pack using the alternate folder/category layout, "A" otherwise (default, and for an unknown slot id). AudioUtil does not interpret it — it exists only so a consumer whose packs ship in more than one layout can read it per resolved slot (usually via GetSlotForActor first) and gate its own category routing. Returns "A" for any slot that doesn't set it.

GetCategoryFileCount

int Function GetCategoryFileCount(string slot, string category) global native

Number of playable files behind slot/category after full category resolution (aliases/fallbacks applied). 0 = a PlayVoice would fail.

CategoryExists

bool Function CategoryExists(string slot, string category) global native

True if slot/category resolves to at least one file. Cheap way to guard optional content ("play the rare line only if the pack ships it").

GetResolvingSlot

string Function GetResolvingSlot(string slot, string category) global native

Which slot actually supplies the audio for slot/category, walking the same alias/fallback resolution PlayVoiceFromSlot uses: the queried slot's own id when the pack itself voices the category, the fallback slot's id when it backfills (e.g. "F0" stock moans), or "" when nothing resolves at all. CategoryExists answers "will something play?" — this answers "will it be the pack?", which is what a voice-pack audit really wants to know: a slot can report every category as resolving while 100 % of it is stock backfill. Requires API version >= 3; guard with GetAPIVersion() on older installs.

Debug

DebugPlayFile

int Function DebugPlayFile(string dataRelativePath, Actor akFollow, int flags, int priority) global native

Play a file with explicit BuildSoundDataFromFile flags/priority instead of the TOML's sound_flags / sound_priority — for experimenting when a file is silent or behaves oddly. Not for production use. If a sound won't play or isn't 3D-positioned, sweep values here.

Wrappers

These are non-native Papyrus helpers (they live in AudioUtil.psc, not the DLL). They block the calling Papyrus thread by polling IsHandlePlaying + Utility.Wait.

WaitForHandle

Function WaitForHandle(int handle) global

Block until the instance finishes, or a safety timeout: clip duration + 1 s, or 30 s when the duration isn't known yet. Returns immediately for dead/invalid handles. Polls every 0.1 s, so expect up to ~0.1 s of overshoot past the actual clip end.

PlayVoiceAndWait / PlaySFXAndWait

Function PlayVoiceAndWait(Actor akActor, string category, float volume = 1.0, string group = "", string channel = "") global
Function PlaySFXAndWait(string sfxName, Actor akFollow, float volume = 1.0, string group = "sfx", string channel = "") global

Play, then block until the line finishes. Same arguments and resolution as PlayVoice / PlaySFX; the handle is discarded.

Play

Function Play(string category, Actor akActor, bool waitForCompletion = true, float volume = 1.0, string group = "", string channel = "") global

Drop-in equivalent of a legacy PlaySound(Sound form, Actor, wait) helper: a category string instead of a Sound form, with optional blocking.