Skip to main content

Audio

StoryFlow supports audio playback tied to dialogue nodes, standalone audio triggers, and audio variables. This guide covers how audio assets are handled, configured, and played back in the Unreal Engine plugin.

Supported Formats

The plugin supports two audio formats for import:

  • WAV - The preferred format. WAV files are imported directly as USoundWave assets and work with UGameplayStatics::PlaySound2D, UAudioComponent, and all standard Unreal audio APIs.
  • MP3 - Supported via automatic decoding. MP3 files are decoded to WAV during import using the built-in minimp3 decoder (CC0 licensed). The decoded audio is stored as a USoundWave asset, so it behaves identically to a WAV import at runtime.

Why MP3 files are auto-decoded

Unreal Engine natively imports MP3 files as FileMediaSource, not USoundWave. This limits playback options and prevents use with standard audio APIs like PlaySound2D. The plugin's built-in minimp3 decoder bypasses this limitation entirely - your MP3 files are decoded to PCM data at import time and stored as USoundWave assets, giving you full compatibility with the Unreal audio system.

Audio on Dialogue Nodes

Dialogue nodes in the StoryFlow Editor can have audio attached. When a dialogue node has audio, it is available at runtime through the dialogue state:

C++
// Access audio from the current dialogue state
void UMyDialogueWidget::UpdateDialogue(const FStoryFlowDialogueState& State)
{
    // State.Audio is a USoundBase* - may be null if no audio is assigned
    if (State.Audio)
    {
        // Audio is available for this dialogue node
        // The plugin handles playback automatically via UAudioComponent,
        // but you can also access the asset directly for custom handling
    }
}

The dialogue state exposes audio as FStoryFlowDialogueState.Audio (USoundBase*). This value is nullptr when no audio is assigned to the dialogue node.

Looping and Reset

Each dialogue node in the StoryFlow Editor has two audio-related settings that control playback behavior:

  • audioLoop (bool) - When enabled, the dialogue's audio loops continuously for as long as the dialogue is active. Internally, the plugin implements looping through the OnAudioFinished delegate callback on the UAudioComponent, which re-triggers playback when the sound completes.
  • audioReset (bool) - When enabled and the dialogue has no audio assigned, any previously playing dialogue audio is stopped. This gives you explicit control over when audio should be cut - for example, a narrative dialogue node that should play in silence after a voiced line.

Advance on End

Dialogue nodes can be configured to auto-advance when audio finishes playing. This is useful for cinematic or voiced dialogue where you want the conversation to flow automatically without requiring the player to click. Two settings control this behavior:

  • audioAdvanceOnEnd (bool) - When enabled, the dialogue automatically advances to the next node via the header output edge when the audio finishes playing. This setting is only available on dialogue nodes that have no interactive options (no buttons or input elements) and audio looping is disabled.
  • audioAllowSkip (bool) - When enabled alongside audioAdvanceOnEnd, the player can click to skip the audio and advance early. When disabled, the player must wait for the audio to finish before the dialogue advances. Calling AdvanceDialogue while audio is playing will be blocked unless audioAllowSkip is true.

The dialogue state exposes these as FStoryFlowDialogueState.bAudioAdvanceOnEnd and FStoryFlowDialogueState.bAudioAllowSkip, so your widget can adjust its UI accordingly (for example, hiding the "Continue" button when advance-on-end is active and skip is not allowed).

C++
void UMyDialogueWidget::OnDialogueUpdated(const FStoryFlowDialogueState& State)
{
    if (State.bCanAdvance && State.bAudioAdvanceOnEnd && !State.bAudioAllowSkip)
    {
        // Audio will auto-advance — hide the continue button
        ContinueButton->SetVisibility(ESlateVisibility::Collapsed);
    }
    else if (State.bCanAdvance)
    {
        // Player can click to advance (or skip audio)
        ContinueButton->SetVisibility(ESlateVisibility::Visible);
    }
}

Error handling

If audio fails to play (asset missing or playback error), the advance-on-end state is automatically cleared so the dialogue falls back to normal bCanAdvance click behavior. The player is never stuck.

Fresh Entry vs Return

Understanding fresh entry

Audio only plays on a fresh entry to a dialogue node - that is, when the runtime arrives at the node by following an edge from another node. When a Set* node (such as setBool, setInt, etc.) has no outgoing edge and returns to the dialogue for re-rendering, audio does not re-trigger. This prevents audio from restarting every time a variable changes during an active dialogue.

This distinction is important for interactive dialogues where clicking an option triggers a Set* node that modifies a variable and then returns to the same dialogue to re-render with updated text (variable interpolation). Without fresh entry tracking, the audio would restart on every variable change.

Internally, the plugin tracks this with a bEnteringDialogueViaEdge flag. When the runtime follows an edge to reach a dialogue node, the flag is true and audio plays. When a Set* node returns to the dialogue without an edge, the flag is false and audio playback is skipped.

C++
// Simplified internal logic for dialogue audio playback
void UStoryFlowComponent::ProcessDialogueNode(const FStoryFlowNode& Node)
{
    // ... build dialogue state ...

    if (bEnteringDialogueViaEdge && DialogueState.Audio)
    {
        // Fresh entry - play the audio
        PlayDialogueAudio(DialogueState.Audio, Node.Data.bAudioLoop);
    }
    else if (bEnteringDialogueViaEdge && !DialogueState.Audio && Node.Data.bAudioReset)
    {
        // Fresh entry, no audio, reset enabled - stop any playing audio
        StopDialogueAudio();
    }
    // Returning from a Set* node: do nothing with audio
}

Component Audio Settings

The UStoryFlowComponent exposes several audio-related properties in the Details panel under the StoryFlow | Audio category. These settings apply globally to all dialogue audio played by the component.

  • bUse3DAudio (bool, default: false) - When enabled, dialogue audio is played as a 3D spatialized sound attached to the owning actor using SpawnSoundAttached. When disabled (default), audio plays as a flat 2D sound via SpawnSound2D. Enable this for in-world dialogue where audio should come from the character's position.
  • bStopAudioOnDialogueEnd (bool, default: true) - When enabled, all playing dialogue audio is automatically stopped when the dialogue ends (when the runtime reaches an End node or StopDialogue is called). Disable this if you want audio to continue playing after the dialogue system closes.
  • DialogueSoundClass (USoundClass*) - Assigns all dialogue audio to a specific sound class. This allows you to create a dedicated "Dialogue" volume slider in your game's audio settings, separate from music, SFX, and other audio categories.
  • DialogueVolumeMultiplier (float, default: 1.0, range: 0.0 - 2.0) - A volume scaling multiplier applied to all dialogue audio. This stacks multiplicatively with the SoundClass mix volume, giving you fine-grained control over dialogue loudness.
  • DialogueConcurrency (USoundConcurrency*) - Controls overlap behavior when multiple audio sources play simultaneously. Use this to limit the number of concurrent dialogue sounds or define priority rules for which audio takes precedence.
  • DialogueAttenuation (USoundAttenuation*) - Attenuation settings for 3D dialogue audio. Controls distance falloff, spatialization method, occlusion, and other spatial audio properties. Only relevant when bUse3DAudio is enabled. Create a Sound Attenuation asset in the Content Browser and assign it here.
C++
// Example: Configure audio settings in C++
UStoryFlowComponent* StoryFlow = FindComponentByClass<UStoryFlowComponent>();

// Enable 3D spatialized audio (attached to this actor)
StoryFlow->bUse3DAudio = true;

// Assign attenuation for 3D falloff control
StoryFlow->DialogueAttenuation = MyAttenuationSettings;

// Stop audio when dialogue ends (default behavior)
StoryFlow->bStopAudioOnDialogueEnd = true;

// Assign a sound class for volume control
StoryFlow->DialogueSoundClass = MyDialogueSoundClass;

// Set volume to 80%
StoryFlow->DialogueVolumeMultiplier = 0.8f;

// Set concurrency rules
StoryFlow->DialogueConcurrency = MyDialogueConcurrency;

PlayAudio Node

The playAudio node is a standalone audio trigger that exists separately from dialogue-attached audio. When the runtime encounters a playAudio node, it broadcasts the OnAudioPlayRequested delegate:

C++
// Delegate signature
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(
    FOnAudioPlayRequested,
    const FString&, AudioPath,
    bool, bLoop
);

// Bind to the delegate in your game code
StoryFlowComponent->OnAudioPlayRequested.AddDynamic(
    this, &AMyActor::HandleAudioPlayRequested);

void AMyActor::HandleAudioPlayRequested(const FString& AudioPath, bool bLoop)
{
    // Use AudioPath to look up and play the sound asset
    // bLoop indicates whether the audio should loop
    // Handle this for ambient sounds, music, stingers, etc.
}

PlayAudio is a broadcast, not automatic playback

Unlike dialogue-attached audio which the plugin plays automatically, the playAudio node only broadcasts a delegate. Your game code is responsible for resolving the audio path, loading the asset, and playing it. This gives you full control over how standalone audio is handled - for example, routing music through a separate audio manager or layering ambient sounds.

SetAudio Node

The setAudio node sets the value of an audio variable. Audio variables store references to sound assets that can be used by dialogue nodes or read by other nodes in the graph.

Like other Set* nodes, if a setAudio node has no outgoing edge and was reached from a dialogue node, it returns to that dialogue for re-rendering. This allows dialogue nodes to reference audio variables that can change dynamically during a conversation.

Internal Playback

Understanding how the plugin handles audio playback internally can help when debugging or extending the audio system.

  • UAudioComponent - Dialogue audio is played through a UAudioComponent. When bUse3DAudio is enabled, the component is created via SpawnSoundAttached on the owning actor's root component for 3D spatialized playback. When disabled (default), it uses SpawnSound2D for flat non-spatialized audio.
  • Looping and Advance-on-End - The OnAudioFinished delegate is always bound on the UAudioComponent. When audio finishes, the callback checks: if looping is enabled, it re-triggers playback; if advance-on-end is active, it calls AdvanceDialogue to automatically proceed. This ensures both features share the same reliable callback mechanism.
  • PlayDialogueAudio() / StopDialogueAudio() - These are BlueprintNativeEvent functions, meaning you can override them in a Blueprint subclass of UStoryFlowComponent for fully custom audio handling (for example, integrating with Wwise, FMOD, or MetaSounds). The default implementations handle 2D/3D playback, looping, and cleanup. StopDialogueAudio is called internally when bStopAudioOnDialogueEnd is enabled, when audioReset triggers, or when new dialogue audio replaces the currently playing sound.

Need Help?

Join our Discord community to ask questions, share your projects, report bugs, and get support from the team and other users.

Join Discord