BeatSyncToolkit
Beat/Bar-Synced Adaptive Music Conductor for Unreal Engine 5. Drive your game's music reactively from Blueprint — state transitions, layered stems, zones, and quantized timing. All without a custom GameInstance.
# Before You Start — Prerequisites BEGINNER
This documentation assumes you can navigate Unreal Editor comfortably and make basic Blueprint connections. If you’re brand new, don’t worry — you can still follow along, but these basics will help a lot.
- Blueprint basics — variables, functions, events, calling nodes.
- Actor placement — dragging Actors into a Level and editing their Details panel.
- Basic audio concepts — what BPM is, and how to import WAV files into UE.
Data Assets (quick explanation)
A Data Asset is an Unreal asset used to store structured configuration data. BeatSyncToolkit uses Data Assets for Music Profiles (playlists, BPM, stems, defaults, etc.). You edit the profile once, then every placed Conductor can reference it.
If you don’t know what a Data Asset is yet, you can still use the default profile for testing — see the Quick Start.
BeatSyncToolkit is built and tested on UE 5.5.x. It uses standard Blueprint features (AudioComponents, Timers, Overlap Events, Data Assets) with no engine modifications, so it is expected to work on UE 5.4 and UE 5.3 as well. However, only 5.5.x is officially tested and supported. If you encounter issues on older engine versions, please report them.
# 1. What BeatSyncToolkit Does
BeatSyncToolkit lets you drive adaptive music in Unreal Engine using gameplay-friendly Blueprint nodes. It provides beat/bar timing, state-based music selection with per-state playlists, optional stem layers, and a zone system that can override the global music state and add extra layers — all without requiring a custom GameInstance.
Key Features
- Beat/bar clock (Timer-based) with an OnBar event for quantized changes — no Quartz dependency.
- State system (E_BST_MusicState): switch music by gameplay state, optionally quantized to the next bar. New states can be added by extending the enum + defining a matching StateConfig in your profile — see Adding a New State.
- Per-state playlist: each state can contain multiple tracks; auto-advance to the next track when the current ends.
- Two-base crossfade playback (Base A / Base B) for smooth transitions between tracks and states.
- Optional stem layers (E_BST_MusicLayer: Drums, Bass, Harmony, Melody, FX). Layers are always started muted and faded in/out — no restart.
- Event-based layers: add/remove/clear layers via global Blueprint nodes (BFL_BST) with optional next-bar quantization.
- Multi-layer event calls: add any number of layers in one node (array input), with optional next-bar quantization.
- Start/Stop Music: optionally start silent and trigger BST_StartMusic / BST_StopMusic from gameplay or demo interactions.
- Zone system (BP_BST_MusicZone): priority-based TopZone selection, zone state override, extra layers, and optional per-zone profile override.
- Zone + event layer arbitration: effective layers are the OR combination of zone layers and event layers.
- Quantized zone layer updates: zone layer changes can be applied immediately or on the next bar.
- Playlist Track Lock: lock the currently playing track so it restarts instead of advancing — event-based and optional zone-based.
- Low-friction UX: all gameplay calls go through a Blueprint Function Library. No manual Conductor references required.
What You Need to Place in Your Level
BP_BST_Conductor — exactly one Conductor Actor in the level. This is the only mandatory placement. Optionally add one or more BP_BST_MusicZone actors for zone-based overrides and layer additions.
1.1 Common Use Cases
Below are simple “copy-the-logic” examples of the most common API calls. For more detailed scenarios (zones, track lock, quantization), see Section 12: Practical Examples.
// In your Combat Manager / AI Director / GameMode: OnEnemySpawned (or OnCombatStart) BST_SetMusicState(WorldContextObject=Self, NewState=Combat)
// When player enters boss arena trigger: OnBossArenaEntered BST_AddLayer(WorldContextObject=Self, Layer=Drums, QuantizeToNextBar=true)
// Example: keep the current intense track looping during phase 2 OnBossHealthBelow50Percent BST_LockCurrentTrack(WorldContextObject=Self) OnBossDefeated BST_UnlockCurrentTrack(WorldContextObject=Self)
If you forget to connect Self (or any valid object) to WorldContextObject, the BFL nodes may fail to find the Conductor and do nothing.
When calling any BFL_BST node (BST_SetMusicState, BST_AddLayer(s), BST_LockCurrentTrack, etc.), you must connect a valid object (usually Self) to WorldContextObject. If it’s left empty, the node may silently fail because it cannot locate the current World to find the placed Conductor.
# 2. Quick Start BEGINNER
Get BeatSyncToolkit up and running in about 10 minutes.
- In the Content Browser, open Plugins → BeatSyncToolkit → Blueprints.
- Drag BP_BST_Conductor into your Level.
- Select the Conductor and set:
- ActiveProfile = DA_BST_MusicProfile_Default
- AutoStartState = Explore
- bStartOnBeginPlay = true
- Press Play — music should start.
If it doesn’t play, jump to Troubleshooting.
Add Content
Add or import the BeatSyncToolkit content folder into your project — keep the folder structure intact. Open or create a test level. Drag BP_BST_Conductor into the level.
Assign a Profile & Auto-Start
Select BP_BST_Conductor in the level. If you want music to start automatically, enable bStartOnBeginPlay. For demos, disable it and call BST_StartMusic when the player is ready.
In Details → BST → Profile, set ActiveProfile to DA_BST_MusicProfile_Default and set AutoStartState (e.g. Explore). Press Play — the Conductor starts the chosen state, initializes the beat clock from the track's BPM/BPB, and begins playback.
Drive Music from Gameplay (No References)
From any Blueprint, call the global nodes in BFL_BST. The WorldContextObject input is all that's needed — no manual Conductor references.
BST_StartMusic(WorldContextObject) BST_StopMusic(WorldContextObject) BST_SetMusicState(WorldContextObject, NewState) BST_AddLayer(WorldContextObject, Layer, QuantizeToNextBar) BST_AddLayers(WorldContextObject, Layers[], QuantizeToNextBar) BST_RemoveLayer(WorldContextObject, Layer, QuantizeToNextBar) BST_ClearLayers(WorldContextObject, QuantizeToNextBar) BST_LockCurrentTrack(WorldContextObject) BST_UnlockCurrentTrack(WorldContextObject)
Add Zones (Optional)
Drag BP_BST_MusicZone into the level and scale its Box Collision to cover an area. Set ZonePriority (higher wins when zones overlap). Optionally set ZoneState to override the global state, and fill ExtraLayers to add layers while inside. Press Play and walk into the zone to see changes in action.
Troubleshooting — Common Problems & Solutions
Most issues come from a missing Conductor in the Level, an unassigned profile, or a context object that can’t find the World.
- Is BP_BST_Conductor placed in the Level? (check the Outliner)
- Is ActiveProfile assigned?
- Is AutoStartState NOT NONE?
- Open the console with ` and look for errors.
- Is WorldContextObject connected? Usually you should pass Self.
- Is a Conductor placed in the Level (remember: BeatSyncToolkit does not auto-spawn it)?
- Enable bDebugPrint on the Conductor to see why a request is ignored.
- Does the current TrackEntry actually have a Sound assigned for that Layer?
- Are you calling BST_AddLayer / BST_AddLayers with the correct layer enum?
- Check layer volumes in the Track Entry (and make sure your stems are exported correctly).
- Is the Zone collision set to Generate Overlap Events = true?
- Does the collision channel overlap your player/pawn?
- If you have nested zones, is ZonePriority correct (highest wins)?
# 3. Included Assets & Files
Keep the content folder structure intact so all internal references (profiles, actors, enums, structs) remain valid. The system is Blueprint-first and does not require a plugin build step.
| Type | Name | Purpose |
|---|---|---|
| Blueprint Actor | BP_BST_Conductor | Main runtime brain. Holds the beat clock, current state/track, playback components, layers, zones, and all arbitration logic. |
| Blueprint Actor | BP_BST_MusicZone | Overlap-triggered zone: override state, add extra layers, override profile, and optionally influence zone quantization / track lock. |
| Function Library | BFL_BST | Global gameplay-facing API. Finds the Conductor and forwards state / layer / lock requests. |
| Data Asset | DA_BST_MusicProfile_Default | Example profile containing StateConfigs and Track playlists. |
| Blueprint (Base) | PDA_BST_MusicProfile_Base | Base class type used by the Conductor for profile assets. |
| Enum | E_BST_MusicState | Music states (e.g. Explore, Combat). Extend by adding new entries — see Adding a New State. |
| Enum | E_BST_MusicLayer | Fixed layer set: Drums, Bass, Harmony, Melody, FX. |
| Struct | F_BST_TrackEntry | One track: BaseSound + BPM / BPB + optional layer defs for each layer. |
| Struct | F_BST_StateConfig | One state configuration: the state enum + its playlist of TrackEntry items. |
| Struct | F_BST_LayerDef | Per-layer definition inside TrackEntry: an optional Sound asset and a target volume. |
| Internal | Pending Layer Queues | Quantized layer ops are implemented via pending arrays / flags processed on BST_OnBar. No asset — runtime only. |
# 4. Core Concepts BEGINNER
4.1 Conductor Actor
BP_BST_Conductor is the single authority that decides what music is playing. You place it manually in the level — it does not auto-spawn. Gameplay code never stores references to it. Instead, calls go through BFL_BST, which finds the Conductor in the current world and forwards the request.
4.2 Profiles → States → Playlists → Tracks
A Music Profile is a data asset that maps each Music State to a playlist of TrackEntry items.
Profile (DA_BST_MusicProfile) └── StateConfig (F_BST_StateConfig) ├── State: E_BST_MusicState ← e.g. Explore, Combat └── Playlist: TrackEntry[] └── TrackEntry (F_BST_TrackEntry) ├── BaseSound ← main audio (required) ├── BPM ← beats per minute ├── BeatsPerBar ← time signature └── Layer Stems (Drums/Bass/Harmony/Melody/FX) ← optional stems
4.3 Layers (Stems)
Layers are optional stem audio components. If a TrackEntry provides a sound for a layer, the Conductor starts it immediately when the track begins — but at muted standby volume. Add/Remove operations only adjust volume (fade up/down) and do not restart audio.
The fixed layer set is Drums, Bass, Harmony, Melody, FX. If a track has no stem for a layer, that layer is safely ignored.
Gameplay Layer Control (Event-Based)
Use BFL_BST layer nodes to add/remove/clear layers from any Blueprint. Each call includes a QuantizeToNextBar boolean to apply the change on the next bar, or fade right away.
Add Multiple Layers (Single Node)
Use BST_AddLayers with an array input to add 2, 3, or all layers at once. Supply the array using Make Array (E_BST_MusicLayer). Duplicates are ignored.
Tracks can optionally auto-enable certain layers when the track starts (for example: Drums ON by default). This is an advanced feature covered in Section 5.7.
4.4 Beat/Bar Clock (No Quartz)
The Conductor maintains a beat clock using timers. Each beat advances CurrentBeat, and each bar fires BST_OnBar. Quantized operations — state changes and optional layer changes — are applied precisely on BST_OnBar.
BeatSyncToolkit uses its own timer-based clock instead of Unreal's Quartz system. Zero additional setup — just set BPM and BeatsPerBar in your TrackEntry and the clock handles the rest.
4.5 Zones
BP_BST_MusicZone is an overlap-based, location-driven control layer. Think of it as “automatic music rules based on where the player is”. The Conductor keeps a list of zones the player is currently inside, selects a single TopZone, then applies that zone’s behavior (state override, extra layers, optional profile override, optional track lock, and optional bar-quantized changes).
What Zones Do (At a Glance)
- Location-based state control: entering an area can automatically switch the music state (e.g. Village → Calm, Forest → Explore).
- Extra layers on top: a zone can add layers (Drums/Bass/etc.) while you are inside, without changing the state.
- Optional profile override: a zone can temporarily switch to a different music profile (useful for “Dungeon Music Pack” vs “Overworld Pack”).
- Nested zones are safe: when multiple zones overlap, the highest priority wins (TopZone).
Zone Priority (Nested Zones)
Each zone has a ZonePriority. If you are inside multiple zones at the same time, the Conductor picks the zone with the highest priority as the active TopZone. This makes nested or overlapping zones predictable: “the most important room wins.”
Zone State Override vs Zone Extra Layers
A zone can influence music in two independent ways:
- ZoneState (Override): forces a specific state while inside. Example: “BossArena → Combat”.
- ExtraLayers[] (Additive): adds layers while inside, without forcing a new state. Example: “WindyBridge → FX layer ON”.
If ZoneState is set to NONE, the zone does not force a state — it can still add extra layers (and optionally override the profile only when it is also forcing a state, depending on your Conductor rules).
Village Zone (Priority 0): ZoneState = Calm, ExtraLayers = []
Forest Zone (Priority 10): ZoneState = Explore, ExtraLayers = [Drums]
When the player stands in the overlap region, Forest wins (higher priority) → Explore plays, and Drums layer is enabled while inside.
Zones do not fight your gameplay logic. They simply become the current TopZone (by priority) and contribute their rules (state override and/or extra layers) into the same arbitration pipeline as event layers and default layers.
Place BP_BST_MusicZone in your level and scale it to cover your area (room, corridor, arena, etc.).
Collision / Overlap: make sure the zone’s collision component generates overlaps with the player. Typical checklist:
- Generate Overlap Events = true
- Collision set to Overlap the channel your player uses (often Pawn)
- If using custom collision channels, ensure your player capsule and the zone agree (Overlap ↔ Overlap).
Set ZonePriority. Higher wins when zones overlap. A common pattern:
- Large “region” zones: priority 0–10
- Small “room / interior / special” zones inside them: priority 50–100+
Choose how the zone affects music:
- State override: set ZoneState to a real E_BST_MusicState value (e.g., Calm / Combat).
- Layer-only zone: set ZoneState = NONE and use ExtraLayers only.
Fill ExtraLayers with any layers you want active while inside the zone (Drums/Bass/Harmony/Melody/FX). These stack on top of the current state.
Optional extras:
- OverrideProfile — used only when the zone is also forcing a state (ZoneState ≠ NONE). This avoids unexpected restarts when the zone is layer-only.
- bZoneTrackLock — requests track lock while inside (event lock still has priority).
- QuantizeZoneLayerChangesToBar — if enabled, zone layer sets can snap to the next bar for musical timing.
- AffectsOnlyLocalPlayer — recommended for multiplayer so zones behave per-client.
How to Set Up a Zone (Step-by-Step)
For deeper rules (profile override edge cases, zone quantization behavior, and zone/event arbitration), see Section 8 and Section 9.
4.6 Intensity System ADVANCED
Intensity is an optional advanced feature: a continuous 0..1 value that can automatically enable “intensity-only” layers based on rules. If you’re new, you can skip Intensity and still get great results with States, Zones, and Event Layers.
For the complete workflow (rules, hysteresis, smoothing/ramp, and volume curves), see Section 14 — Intensity System (Advanced).
- You set intensity from gameplay via BST_SetIntensity (BFL node).
- Intensity only affects layers that are enabled by intensity rules — it does not override Default / Zone / Event enabled layers.
- If you don’t call BST_SetIntensity, Intensity stays at its default and does nothing.
# 5. Creating & Editing Music Profiles
5.1 Create a Custom Profile
In the Content Browser, duplicate DA_BST_MusicProfile_Default.
Rename it — e.g. DA_BST_MusicProfile_MyGame.
Open your new profile. For each state you want to support, add or edit a StateConfig entry.
5.2 Fill a StateConfig (Playlist)
In each StateConfig: set the State (E_BST_MusicState), add one or more TrackEntry items to the Tracks array, and provide accurate BPM and BeatsPerBar for each track.
5.3 Fill a TrackEntry
Each TrackEntry is the complete definition for one piece of music in a playlist.
| Field | Type | Meaning |
|---|---|---|
| BaseSound | SoundWave / SoundCue | Main full mix or main stem. Always required. |
| BPM | Float | Beats per minute. Drives the beat/bar clock and all quantization. |
| BeatsPerBar | Integer | Time signature in beats per bar (commonly 4). |
| Drums / Bass / Harmony / Melody / FX | F_BST_LayerDef | Optional stems for each layer. Leave empty if you don't have that stem. |
If you only have a single WAV with no stems, set BaseSound only and leave all layer sounds empty. You can still use state switching and playlists. Layer add/remove calls are safely ignored when the current track has no matching stem.
5.3.1 F_BST_LayerDef Fields (Per-Layer Configuration)
Each layer slot (Drums, Bass, Harmony, Melody, FX) inside a TrackEntry uses the F_BST_LayerDef struct. Here are all configurable fields:
| Field | Type | Default | Meaning |
|---|---|---|---|
| Sound | SoundWave / SoundCue | None | The audio asset for this stem. If empty, the layer is ignored for this track. |
| TargetVolume | Float | 1.0 | Volume when the layer is enabled (0.0 to 1.0). Use this to balance stems against each other. |
| FadeInDuration | Float | 0.5 | Seconds to fade in when the layer is enabled. Shorter = snappier, longer = smoother blend. |
| FadeOutDuration | Float | 0.5 | Seconds to fade out when the layer is disabled. Independent of FadeInDuration. |
| StandbyVolume | Float | 0.0 | Volume when the layer is muted but still playing in the background. Typically 0.0 (silent). |
Each layer in each track can have its own fade durations. For example, Drums can fade in over 0.2s (punchy) while Harmony fades in over 1.5s (smooth). This gives you precise control over how the mix evolves, without any Blueprint logic.
5.3.2 Audio Preparation Guide
Before importing your music into Unreal, follow these guidelines to ensure BeatSyncToolkit works reliably.
Supported Audio Formats
| Format | Recommended | Notes |
|---|---|---|
| .wav | Yes | Best quality, no compression artifacts. Preferred for all stems and base tracks. |
| .ogg | Acceptable | Smaller file size. Good for long ambient/exploration tracks where file size matters. |
Stem Preparation Rules
BeatSyncToolkit starts all stems simultaneously and fades them in/out. If stems have different lengths, they will drift out of sync. Always export stems from the same session with the same start and end points.
- Same BPM: All stems for one track must share the exact same BPM. The Conductor uses a single beat clock per track.
- Same length: Export all stems with identical start/end points. Pad shorter stems with silence if needed.
- Same sample rate: Use a consistent sample rate across all stems (44100 Hz or 48000 Hz recommended).
- Mono vs Stereo: Both work. Use stereo for full-mix base tracks and wide stems (pads, ambience). Mono is fine for punchy elements (kick, snare) if you want to save memory.
- No built-in fades: Do not add fade-in/fade-out to your stem files. BeatSyncToolkit handles all fading at runtime via LayerDef settings.
- Loop-ready: If you want seamless looping, make sure your audio starts and ends cleanly on a beat boundary. Avoid silence at the start or end of the file.
In your DAW (Ableton, FL Studio, Logic, Reaper, etc.), solo each stem group and "Export All" or "Bounce in Place" with the same range selection. This guarantees all files have identical length and timing.
5.4 Assign Profile to the Conductor
Select BP_BST_Conductor in your level. Set ActiveProfile to your custom profile asset. Set AutoStartState to the initial state. Press Play.
5.5 Adding a New State
You can extend BeatSyncToolkit with your own states. The system uses a loop-based lookup — no hardcoded switch-case — so adding a new enum entry won't break any existing Blueprint graphs or pins.
Adding a new entry to E_BST_MusicState makes it appear in all BFL nodes immediately — but no music will play for that state until you also define a StateConfig with at least one track in your profile. The system won't crash — it simply won't switch, or will stay on the current track.
Add the new entry to the enum
Open E_BST_MusicState and add your new state (e.g. Stealth). Save and compile. It will now appear as a selectable option in all BFL nodes across your project — automatically.
Open your Music Profile
Open the profile Data Asset you're using (e.g. DA_BST_MusicProfile_MyGame).
Add a new StateConfig entry
In the StateConfigs array, add a new row. Set its State to your new enum value (e.g. Stealth).
Add at least one TrackEntry
Inside that StateConfig, add a TrackEntry to the Playlist array. At minimum, set BaseSound, BPM, and BeatsPerBar. Optionally add stem sounds (Drums/Bass/Harmony/Melody/FX) if your audio has them.
Save and test
Save the profile. Press Play and call BST_SetMusicState(Stealth) — the Conductor will find the new StateConfig via lookup and switch to it.
Files You Will Edit (State Extension)
- All BFL_BST state nodes (dropdowns update automatically after compile).
- BP_BST_MusicZone.ZoneState dropdown.
- Conductor settings such as AutoStartState (if you use it).
If your new state is selectable but nothing changes, 99% of the time the profile is missing a matching StateConfig entry (or its playlist has no tracks).
The Conductor resolves states by searching through the StateConfig array at runtime — not via a switch-case baked into the Blueprint graph. This means adding a new E_BST_MusicState entry won't cause any pin breaks or compilation errors anywhere. The only requirement is a matching StateConfig in your active profile.
5.6 Adding a Custom Layer
The five built-in layers (Drums, Bass, Harmony, Melody, FX) cover most games. If you truly need a sixth stem layer — for example Percussion — you can extend BeatSyncToolkit.
This is an advanced extension. You are adding a new audio channel, a new stem slot in track data, and wiring it into the Conductor’s layer pipeline (prepare → gating → enable/arbitration → fade routing). If you only want more variety, prefer States, Zones, and State Requests instead of inventing new layers.
What stays the same
- BFL nodes automatically show the new enum entry in dropdowns (no BFL edits needed).
- Zone ExtraLayers and Event layers already use E_BST_MusicLayer arrays — your new layer can be requested immediately.
- The core rule is unchanged: stems start playing in standby (muted), and layer toggles only change volume (no restart).
Step-by-step (recommended order)
Extend the Layer Enum
Open E_BST_MusicLayer and add your new entry (example: Percussion). Save + compile. It will now appear in all layer dropdowns automatically.
Add a Stem Slot to Track Data
Open F_BST_TrackEntry and add a new field for your layer using the same type as other stems (recommended: F_BST_LayerDef).
Example: add a field named Percussion (type F_BST_LayerDef), so each TrackEntry can optionally provide a stem Sound + Target Volume.
Add a Dedicated AudioComponent
In BP_BST_Conductor, add a new AudioComponent for the layer (example: AC_Percussion). Configure it the same way as existing layer components (AC_Drums, AC_Bass, etc.).
This component is the playback channel for the new stem.
Prepare the Stem on Track Start
Open BST_EnsureLayerComponentsPlayingMuted and add a block for your new layer:
- If the current track has a valid stem Sound for the layer → SetSound (if changed), Play, then set volume to StandbyVolume (very low).
- If the current track has no stem for the layer → fade out to standby and only stop after a short delay (use BST_FadeOutAndStopLayerComponent pattern).
BeatSyncToolkit’s core promise is: stems do not restart when you toggle layers — we only fade volumes. Preparing all available stems in standby on track start is what makes that possible.
Gate by Stem Availability
Open BST_CurrentTrackHasLayerSound and add a new case for your layer that returns whether the current track actually contains a stem Sound for it.
This prevents the system from ever trying to enable a layer that does not exist in the current track.
Include the Layer in Enable/Arbitration
Update the layer enable logic so your new layer follows the same rules as built-ins:
Enabled = (Default AND NOT Suppressed) OR Zone OR Event, gated by BST_CurrentTrackHasLayerSound.
If your build uses helper functions, extend them:
- BST_ShouldEnableLayerNow — add your enum case.
- BST_GetLayerTargetVolumeNow — return the correct target volume for the layer (from your new F_BST_LayerDef field).
If you don’t have those helpers, the same logic lives directly inside BST_ApplyEventLayersNow — extend the corresponding block there.
Fade Routing (Apply)
In BST_ApplyEventLayersNow, add a new block that routes ShouldEnable to your new AudioComponent:
- If enabled → AdjustVolume(FadeSeconds, TargetVolume)
- If not enabled → AdjustVolume(FadeSeconds, StandbyVolume)
Do not hard-stop stems here (except when the current track has no stem for that layer, handled via the “fade out and stop” helper).
Default Layers & (Optional) Intensity
Because default layers and suppression work from enum lists, your new layer is supported automatically — as long as the Conductor includes it in the “all layers” paths:
- Update BST_RecomputeDefaultLayersForCurrentTrack so AllAvailable can include the new layer.
- If you use Intensity, you may also need to update any layer-bitmask helpers (example: BST_LayerToBit) and intensity rule evaluation so the new layer can be intensity-driven.
Finally: Fill the stem in your Profile
Open your DA_BST_MusicProfile_* asset. For every TrackEntry that should support the new layer, fill in your new F_BST_LayerDef field (Sound + Target Volume). Tracks where you leave it empty will safely ignore the layer — no errors.
Files You Will Edit (Advanced)
| Type | Name | Why |
|---|---|---|
| EN | E_BST_MusicLayer | Add the enum entry. |
| ST | F_BST_TrackEntry | Add the new F_BST_LayerDef field. |
| BP | BP_BST_Conductor | Add the new AudioComponent. |
| FN | BST_EnsureLayerComponentsPlayingMuted | Prepare the stem in standby on track start. |
| FN | BST_CurrentTrackHasLayerSound | Gate enable by stem availability. |
| FN | BST_ApplyEventLayersNow | Fade routing for the new AudioComponent. |
| FN | BST_RecomputeDefaultLayersForCurrentTrack | So your layer can be default-enabled (AllAvailable). |
| DA | DA_BST_MusicProfile_* | Fill the new stem Sound + Volume per track. |
Why fixed layers by default
Fixed layers give the best balance of low-friction setup, clean UX (simple nodes), reliable runtime behavior (consistent fades and quantization), and no audio-designer dependency for common cases. Custom layers are supported — but for most games, the five built-in layers are enough.
5.7 Advanced: Default Layer Control (Auto-Start on Track Begin)
By default, BeatSyncToolkit starts all available stems muted (StandbyVolume) and only enables layers when you request them via Events or Zones. Default Layer Control adds an optional third source: the track can auto-enable specific layers as soon as the track begins.
If you do not need “auto-on layers” per track, you can ignore this entire section. The toolkit works perfectly with Event Layers + Zone Layers only.
What Problem This Solves
Sometimes you want a track to start with a specific mix automatically, without calling AddLayer in gameplay. Examples:
- Track A starts with Drums already ON.
- Track B starts fully muted (no layers ON).
- Track C starts with Drums + FX ON, but only if those stems exist in that track.
Where You Configure It
Default layers are configured per TrackEntry so each track in the same state playlist can behave differently. In F_BST_TrackEntry, you add:
- TrackDefaultMode (enum): InheritFromConductor, MutedOnly, AllAvailable, CustomList
- TrackDefaultLayers (array of E_BST_MusicLayer) used when mode is CustomList
How the Modes Work
| Mode | Meaning | When to Use |
|---|---|---|
| InheritFromConductor | Use the Conductor’s global default-start setting (if you have one). If you don’t use a global setting, treat this like MutedOnly. | Keep one consistent default across many tracks, while still allowing per-track override when needed. |
| MutedOnly | Start with no default layers enabled. All stems still play muted at standby volume. | Clean “silent layers by default” behavior. Gameplay/Zone decides everything. |
| AllAvailable | Enable every layer that exists in the current track (Drums/Bass/Harmony/Melody/FX, gated by stem validity). | Tracks that should start “fully stacked” without extra gameplay calls. |
| CustomList | Enable only the layers in TrackDefaultLayers (also gated by stem validity). | Per-track curated starting mix (e.g., Drums+FX only). |
Runtime: Recompute Before Applying Layers
On every track start (state switch, StartMusic, playlist auto-advance, and “restart same track while locked”), the Conductor recomputes which default layers are active for that track using:
BST_RecomputeDefaultLayersForCurrentTrack()
This function sets ActiveDefaultLayers for the current track, based on TrackDefaultMode and TrackDefaultLayers, and gates every choice through BST_CurrentTrackHasLayerSound so missing stems are never armed.
BST_ApplyEventLayersNow is where volumes actually fade up/down. It needs the current ActiveDefaultLayers to be ready first. The recommended order on track start is:
EnsureLayerComponentsPlayingMuted → BST_RecomputeDefaultLayersForCurrentTrack → BST_ApplyEventLayersNow
How Default Layers Interact with Zone / Event Layers
Layers are enabled by OR logic across sources. Default layers cannot “turn off” zone or event layers. The effective rule is:
Enabled = Zone OR Event OR (Default AND NOT SuppressedDefault)
Suppressing Default Layers with Remove / Clear
You requested a special behavior: if the player removes a default-enabled layer during the current track, that layer should stop coming back from Default until the track restarts/changes.
This is handled with SuppressedDefaultLayers:
- BST_RemoveLayer: removes from Event Layers, and if the layer is in ActiveDefaultLayers, it also adds it into SuppressedDefaultLayers.
- BST_ClearLayers: clears Event Layers, and sets SuppressedDefaultLayers to the current ActiveDefaultLayers (muting all defaults for the remainder of the track).
- BST_AddLayer(s) does not remove suppression — it enables via Event. Suppression resets only on track start.
Default layers define the “starting mix”. If you want to change the mix at runtime, you use Event Layers:
• To disable a default layer for this track: call BST_RemoveLayer or BST_ClearLayers
• To enable it again: call BST_AddLayer / BST_AddLayers
• To reset defaults: restart/switch track (state switch, playlist advance, restart locked track)
5.8 Intensity Overrides
Intensity is an optional advanced system. You can safely skip it if you only want beat-synced state changes, zones, layers, and requests. If you do use Intensity, TrackEntries can optionally override which intensity rules/profile are active while that track is playing.
See Section 14.2 “Intensity System (Advanced)” for the full setup, rules, smoothing, hysteresis, and overrides.
# 6. Gameplay API (BFL_BST)
BFL_BST is the only thing your gameplay Blueprints need to call. It finds BP_BST_Conductor in the current world and forwards the request internally.
State Requests (Priority Overrides)
State Requests are built for temporary, event-driven overrides (AI combat, boss phases, cutscenes, scripted moments) where you want the music to automatically revert when the event ends.
- Push a request when the event starts.
- Remove the same request when the event ends.
- If multiple requests are active, higher Priority wins (requests only compete with other requests).
- Tip: If you have many instances (multiple AI, repeated triggers), use scoped request keys so each instance cannot overwrite another.
- Requests override the global baseline. If any State Request is active, the winning request (highest Priority) overrides GlobalRequestedState.
- Zones win by default. If you are inside a TopZone that is forcing a state (TopZone.ZoneState is not NONE), the zone keeps control unless the winning request is pushed with ForceWhileInZone = true.
- When requests end, the system falls back automatically. After the last request is removed, the Conductor returns to the zone’s state (if still inside a forced zone), otherwise to GlobalRequestedState.
- Note: BST_SetMusicState updates GlobalRequestedState but does not cancel active requests.
- BST_ClearRequests clears every active request at once (useful for respawn, level reset, cutscene skip).
ProfileInUse is resolved together with the effective desired state. The zone’s OverrideProfile is used only when the zone is actually providing the desired state (TopZone.ZoneState). If the desired state comes from a State Request or from GlobalRequestedState, then ProfileInUse = ActiveProfile. This prevents “state not found” issues when a zone override profile does not contain a requested state.
Normal Requests vs Scoped Requests
Every state request is stored under a key called RequestId. If two systems use the same key, they are editing the same request.
- Normal request → You pick a globally-unique RequestId (good for singletons like cutscenes, boss phases, scripted moments).
- Scoped request → The key is built from ScopeKey + RequestName (good when the same Blueprint can exist many times, like AI enemies or repeated triggers).
If you spawn 5 enemies of the same AI Blueprint and they all push "AI_Combat", they will fight over one request. The last one overwrites the others, and removing it can end combat music too early. Scoped requests avoid this by giving each instance its own unique request slot.
How to use scoped requests
- Recommended (if available in your build): Use the request nodes that include a Scope/ScopeKey input. Plug Self for per-actor scope, and keep your request name short (e.g., Combat).
- Fallback (works in any build): Manually build a unique RequestId by combining a scope value (like the AI actor name) with a short request name (e.g., Enemy_12_Combat). Push and remove using the exact same string.
Example A2 — Multiple AI (Scoped)
// Per AI instance (preferred): BST_PushStateRequest(Scoped(Self, "Combat"), Danger, 20, false, true) // When that same AI ends the situation: BST_RemoveStateRequest(Scoped(Self, "Combat"), true)
For very large crowds, you may prefer a small “Combat Music Manager” that keeps a counter (how many enemies are actively in combat) and pushes a single request while the counter > 0. This avoids storing one request per enemy, but both approaches are valid.
Example A — AI Combat (Auto-Revert)
// When AI spots the player: BST_PushStateRequest("AI_Combat", Danger, 20, false, true) // When AI loses the player: BST_RemoveStateRequest("AI_Combat", true)
Example B — Cutscene (Can Override Forced Zones)
// Cutscene start (override forced zones by using ForceWhileInZone=true): BST_PushStateRequest("Cutscene", Cinematic, 100, true, true) // Cutscene end: BST_RemoveStateRequest("Cutscene", true)
Tip: If you have multiple temporary systems and want to “return to normal” quickly, call BST_ClearRequests instead of tracking every RequestId.
BeatSyncToolkit is client-side audio. In multiplayer, each client plays its own music locally. This means:
- If you call BFL_BST nodes only on the server, clients will not hear changes. Call the nodes on each client.
- On a dedicated server, you usually do not need the Conductor at all (no audio). Place/run it on clients.
- Recommended pattern: replicate a small “music intent” (State, EventLayers, TrackLock) via GameState or a replicated manager, then in OnRep call the matching BFL_BST nodes locally.
- Zones are evaluated per client. Use AffectsOnlyLocalPlayer on zones to avoid cross-player overlap edge cases.
This toolkit does not force any networking architecture — it simply gives you clean local playback and a tiny gameplay API.
When a node includes QuantizeToNextBar: true queues the change and applies it on the next bar; false applies immediately using fades. Use quantized calls for tight musical timing; use immediate calls for reactive gameplay.
| Node | Description |
|---|---|
| BST_GetConductor | Finds the placed BP_BST_Conductor in the current world. Returns Found=false if missing. |
| BST_StartMusic | Starts music playback. Use this when bStartOnBeginPlay is disabled — the demo-friendly "start silent" mode. |
| BST_StopMusic | Fades out and stops all music, stops the beat clock, resets runtime state. Safe to call anytime. |
| BST_SetMusicState | Sets the Global Requested State (your baseline). Zones and State Requests can still override it. Use this for long-lived gameplay modes (Exploration / Combat / Boss, etc.). For short-lived overrides, prefer BST_PushStateRequest + BST_RemoveStateRequest. |
| BST_PushStateRequest | Pushes a temporary state request (RequestId + Priority). Highest Priority wins (between requests). By default it overrides the global baseline; to override a forced zone state, push with ForceWhileInZone = true. |
| BST_RemoveStateRequest | Removes a previously pushed request by RequestId. After removal, the system falls back to the next winner (zone / another request / global state). Can be quantized to the next bar. |
| BST_ClearRequests | Clears all active state requests at once. Useful when you don’t track IDs (respawn, level reset, cutscene skip). Can be quantized to the next bar. |
| BST_SetIntensity | Sets gameplay Intensity (0..1). Intensity auto-enables layers based on rules. Supports optional per-call QuantizeToNextBar and a per-call smooth/instant override (so quantized intensity also applies with the intended feel). |
| BST_GetCurrentIntensity01 | Returns the Conductor's current applied intensity (after smoothing/ramp). Useful for UI/debug. |
| BST_IsIntensityRamping | True while the ramp timer is actively updating CurrentIntensity01. |
| BST_GetActiveIntensityLayers | Returns the layers currently enabled by Intensity rules (debug/readback). |
| BST_AddLayer | Adds a single event layer (fade in). Enable QuantizeToNextBar to apply on the next bar. |
| BST_AddLayers | Adds multiple event layers in one call (array input). Use Make Array to supply 2/3/all layers. |
| BST_RemoveLayer | Removes a single event layer (fade out). If the same layer is required by the active zone, it remains enabled. |
| BST_ClearLayers | Clears all event layers. Zone layers remain active and unaffected. |
| BST_LockCurrentTrack | Locks the currently playing track — prevents auto-advance, restarts the same track on finish. |
| BST_UnlockCurrentTrack | Releases the lock so the playlist can auto-advance again. |
No manual references needed — WorldContextObject is sufficient. If no Conductor exists in the level, all nodes do nothing (Found=false). Layer nodes operate on Event Layers (ActiveEventLayers). Zone Layers are managed separately by zones and are not cleared by BST_ClearLayers.
Every BFL_BST node has a WorldContextObject input. This input must be connected — otherwise BST_GetConductor (which runs internally on every call) cannot locate the world and returns Found=false. The call silently does nothing.
← Inside any Blueprint (Actor, Widget, GameMode, etc.) BST_SetMusicState( WorldContextObject: self ← drag "self" node here NewState: Combat ) ← "self" gives the node a valid World reference. BST_GetConductor uses it to find BP_BST_Conductor in that world.
BST_SetMusicState( WorldContextObject: ← left empty / not connected NewState: Combat ) ← WorldContextObject is null. BST_GetConductor returns Found=false. Nothing happens. No error, no crash — silent fail.
# 7. Conductor Settings & Variables
These are the main user-facing variables shown in the Conductor's Details panel.
| Variable | Meaning |
|---|---|
| ActiveProfile | Profile asset used to resolve StateConfigs. Zones may temporarily override this via OverrideProfile. |
| AutoStartState | State to begin automatically when the level starts. |
| StateChangeFadeDuration | Crossfade duration when switching states or tracks. |
| bQuantizeStateChangesToBar | If true, state change requests are held and applied on the next bar. |
| bStartOnBeginPlay | If true, music starts automatically on BeginPlay. If false, the level starts silent until BST_StartMusic is called. |
| BaseVolume | Master volume multiplier for the base mix. |
| bAutoAdvancePlaylist | If true, the Conductor advances to the next track in the playlist when the active base finishes. |
| bLoopIfSingleTrack | If true and the playlist has only 1 track, restart it when it finishes. |
| bQuantizeZoneLayerChangesToBar | If true, zone layer set updates are queued and applied on the next bar. |
| DefaultIntensityProfile | Default Intensity Rules asset used when profiles are set to UseConductorDefault (or when no override is provided). (aka DefaultIntensityRules in older builds) |
| bUseIntensityHysteresis | If true, rules use a separate close threshold (OpenThreshold − HysteresisDelta) to prevent flicker around boundaries. |
| DefaultIntensityHysteresisDelta | How far below OpenThreshold a layer must fall before it turns off (when hysteresis is enabled). |
| bSmoothIntensity | If true, the Conductor ramps CurrentIntensity01 toward TargetIntensity01 over time. |
| IntensitySmoothingSeconds | Approximate time to reach the target intensity (lower = snappier, higher = smoother). |
| IntensityRampUpdateInterval | How often the ramp tick runs (smaller = smoother, larger = cheaper). |
| MinCurveReapplyDelta | Min-change gate for V2.2: prevents re-applying intensity layers on tiny value changes during ramping. (aka IntensityMinDeltaToReapply) |
| bDebugPrint / bDebugZones / bDebugProfiles | Prints debug info to the console for troubleshooting. |
There are two separate fade systems in BeatSyncToolkit: (1) StateChangeFadeDuration controls the base track crossfade when switching states or tracks (set on the Conductor), and (2) FadeInDuration / FadeOutDuration control individual layer fades (set per-layer in each F_BST_LayerDef inside the TrackEntry). These are independent, so you can have a long 2-second state crossfade with snappy 0.1-second drum layer fades, or vice versa.
Runtime Variables (FYI)
At runtime the Conductor maintains CurrentState, CurrentTrack, beat clock values (BPM / BeatsPerBar / CurrentBeat), pending requests (PendingState, pending layer ops, pending zone layers), zone tracking (ActiveZones, TopZone, ActiveZoneLayers), and track lock flags (bEventTrackLock, bZoneTrackLock, bTrackLockActive).
# 8. Zone System INTERMEDIATE
Zones are overlap-based. When the player enters or exits zones, the Conductor maintains an ActiveZones array and selects the TopZone by highest ZonePriority. Only the TopZone's settings are applied at any time.
8.1 Zone Properties
| Property | Meaning |
|---|---|
| ZonePriority | Higher value wins when multiple zones overlap. |
| ZoneState | If not NONE, becomes the effective desired state while inside the TopZone (zone wins by default; can be overridden only by a forced State Request). |
| ExtraLayers | Array of E_BST_MusicLayer to add while inside the zone. |
| OverrideProfile | Optional: use a different profile when this zone is TopZone and the zone is actually providing the desired state (ZoneState is not NONE and wins the resolver). If a forced State Request overrides the zone, the Conductor stays on ActiveProfile. |
| bZoneTrackLock | Request Track Lock while inside this zone. Lower priority than Event Track Lock. |
| State Quantize Override | Optional tri-state override for state change quantization while this zone is TopZone (Inherit / Force On / Force Off). If forced ON/OFF, it overrides the Conductor default at runtime. |
| QuantizeZoneLayerChangesToBar | Toggle that drives the Conductor's zone-layer quantization behavior when this zone becomes TopZone. |
| AffectsOnlyLocalPlayer | If enabled, the zone only reacts to the local player — useful for single-player testing. |
8.2 Overlapping Zones & Priority
When inside multiple zones, the Conductor picks TopZone by the highest ZonePriority. If TopZone changes, the Conductor reapplies zone override (state / profile) and zone layers. Leaving a TopZone triggers reselection of the next best zone, or disables zone override entirely if none remain.
8.3 Zone + Gameplay State Interaction
Gameplay requests via BST_SetMusicState set GlobalRequestedState. If ZoneOverrideActive is true, the TopZone's ZoneState can override what actually plays. When you leave all zones, the Conductor returns to GlobalRequestedState automatically.
If you need to override a zone temporarily (AI combat, scripted moments), use State Requests (BST_PushStateRequest / BST_PushScopedStateRequest). Zones win by default when they force a state. To override a forced zone, push a request with ForceWhileInZone = true. When the request is removed, the system falls back to the zone (or the global state).
# 9. Arbitration Rules ADVANCED
When gameplay, zones, and events request different things at the same time, the Conductor follows deterministic priority rules.
9.1 State Resolution Priority
The Conductor resolves the effective desired state using these deterministic rules:
- Forced Zone State — If ZoneOverrideActive is true and TopZone.ZoneState is not NONE, the zone becomes the effective desired state by default.
- State Requests — If there is no forced zone state, the winning request (highest Priority among requests) becomes the effective desired state.
- GlobalRequestedState — Fallback from BST_SetMusicState.
If you want a request to override a zone that is forcing a state, push that request with ForceWhileInZone = true. If it is false, the request remains active but the zone stays in control until you exit the forced zone (or the zone state becomes NONE).
9.2 Profile Resolution Priority
ProfileInUse is resolved together with the effective desired state (via BST_ResolveDesiredStateAndProfile_Now). Rule: Use the zone’s OverrideProfile only when the zone is the source of the desired state (TopZone.ZoneState). If the desired state comes from a State Request or from GlobalRequestedState, then ProfileInUse = ActiveProfile. This prevents “state not found” issues when a zone override profile does not contain a requested state.
9.3 Layer Resolution (OR Combination)
A layer is active if any source requests it:
| Layer Source | Description |
|---|---|
| ActiveDefaultLayers | Layers that are enabled by default when a track starts (per-track overrides supported). These can be temporarily muted via SuppressedDefaultLayers. |
| ActiveZoneLayers | Layers from the current TopZone's ExtraLayers (or pending zone layer set if quantized). |
| ActiveEventLayers | Layers explicitly toggled by gameplay through BFL_BST (or pending ops if quantized). |
| ActiveIntensityLayers | Layers enabled automatically by Intensity rules (0..1). This is computed and updated as intensity changes. |
Because layers use OR logic, removing a layer from Event Layers will not turn it off if the active zone still requests that same layer.
# 10. Quantization (Next-Bar Changes)
Quantization applies changes exactly on the next bar boundary — keeping transitions musically clean.
10.1 State Quantization
When bQuantizeStateChangesToBar is ON, state requests wait until the next bar boundary. On BST_OnBar, the Conductor re-resolves the effective desired state/profile using the current TopZone (if any) + active State Requests + your latest GlobalRequestedState, then switches only if needed. This prevents “stale” zone states from applying if you enter/exit zones before the bar.
Effective State Quantization (Zones vs Conductor)
There are two related values:
- bQuantizeStateChangesToBar — your default preference (instance-editable on the Conductor).
- Quantize State Changes to Bar Effective — the runtime value actually used by the system. This is resolved whenever TopZone changes.
The Conductor resolves the effective flag via BST_ResolveEffectiveStateQuantize:
- If the current TopZone forces quantize ON/OFF (tri-state override), that wins.
- Otherwise, it falls back to bQuantizeStateChangesToBar.
Quantize State Changes to Bar Effective is treated as read-only. If you set it manually, it will be overwritten the next time zones are recomputed or BST_ResolveEffectiveStateQuantize runs. For runtime toggles (like demo buttons), set bQuantizeStateChangesToBar instead.
10.2 Event Layer Ops Quantization
Queued layer ops are applied on each bar in a deterministic order:
1) Clear ← optional PendingClearEventLayers 2) Remove ← PendingRemoveLayers[] 3) Add ← PendingAddLayers[] (only if track has that stem) 4) ApplyNow ← BST_ApplyEventLayersNow()
10.3 Zone Layer Set Quantization
When zone layer changes are quantized, the Conductor stores PendingZoneLayers and sets bHasPendingZoneLayerSet=true. On bar, BST_ApplyPendingZoneLayers_OnBar copies pending layers into ActiveZoneLayers, clears the flag, and calls BST_ApplyEventLayersNow.
BeatSyncToolkit supports quantization for three categories: state changes (bQuantizeStateChangesToBar), event layer operations (per-call QuantizeToNextBar), and zone layer set updates (bQuantizeZoneLayerChangesToBar).
# 11. Playlist, Auto-Advance & Track Lock
11.1 Base Playback & Crossfade
The Conductor uses two base AudioComponents (Base A and Base B). When switching tracks or states, it crossfades from the active base to the other via BST_CrossfadeBaseTo. bUsingBaseA tracks which base is active — OnAudioFinished events from the inactive base are ignored.
11.2 Auto-Advance (Play Next Track)
If bAutoAdvancePlaylist is ON, when the active base finishes, BST_PlayNextTrack_SameState fires. The next track is chosen from the current state's playlist, avoiding immediate repeats when possible. After selection: beat clock restarts, base crossfades, layer components are ensured playing muted, and active layers are re-applied.
11.3 Track Lock
Track Lock prevents auto-advance for the current track. When locked and the active base finishes, the Conductor restarts CurrentTrack instead of advancing. Event-based lock (from BFL_BST) takes priority over zone-based lock (from TopZone).
Track Lock locks the currently playing track at the moment the lock is engaged — not a specific track by name. It locks whatever is playing right now.
11.4 Advance Lock (Anti Double-Advance)
The Conductor briefly sets an internal Advance Lock during transitions to prevent double-advance (e.g. if both base finished events fire near a switch). This lock auto-clears after AdvanceLockSeconds.
# 12. Practical Examples
Example A — Exploration / Combat with Quantized Transitions
// Enable quantization on the Conductor Conductor.bQuantizeStateChangesToBar = true // If your game uses per-zone state-quantize overrides, refresh the runtime effective flag: Conductor.BST_ResolveEffectiveStateQuantize() // Normal gameplay: BST_SetMusicState(Explore) // Combat starts — change applies cleanly on next bar: BST_SetMusicState(Combat) // Combat ends: BST_SetMusicState(Explore)
Example B — Layered Tension (Event Layers)
// Ensure your Combat tracks include a Drums stem BST_AddLayer(Drums) ← fade in drums BST_RemoveLayer(Drums) ← fade out drums // No Drums stem on current track → safely ignored
Example C — Zone Overrides + Extra Layers
Place a BP_BST_MusicZone for a "Danger Area". Set ZonePriority high, ZoneState = Danger, and ExtraLayers = [FX].
Walk into the zone: state is overridden and FX layer enables. Walk out: music returns to GlobalRequestedState and FX turns off — unless it's still requested by event layers.
Example D — Overlapping Zones (TopZone Selection)
Place Zone A (Priority 10) and Zone B (Priority 20) overlapping. Inside both: Zone B is TopZone. Leave Zone B while still in Zone A: the Conductor automatically switches back to Zone A's behavior.
Example E — Track Lock during a Boss Fight
// Boss fight begins: BST_LockCurrentTrack() ← track restarts on finish // Boss fight ends: BST_UnlockCurrentTrack() ← auto-advance resumes
# 13. Debugging & Troubleshooting
Common Setup Issues
| Symptom | Likely Cause |
|---|---|
| Nothing plays | Forgot to place BP_BST_Conductor, or ActiveProfile / AutoStartState is not set. |
| Layers do nothing | Current track has no stem for that layer — LayerDef sound is empty. |
| Zone changes don't apply | Player isn't overlapping the zone's collision, or TopZone selection isn't what you expect. |
| State changes seem delayed | bQuantizeStateChangesToBar is on — changes apply on the next bar. |
| Layer changes seem delayed | You enabled QuantizeToNextBar on your layer call. |
| Host hears music, clients don’t | You are triggering BFL_BST calls only on the server/host. Call them on each client (or replicate a “music intent” and call nodes in OnRep). |
Debug Flags
Enable these on BP_BST_Conductor to get console output:
bDebugPrint ← state switches, track picks, bar events bDebugZones ← zone enter/exit, TopZone changes, overrides applied bDebugProfiles ← profile resolution, state config lookups
Sanity Checklist
- Exactly one Conductor exists in the level.
- ActiveProfile contains an entry for the target state.
- Each TrackEntry has BaseSound set and correct BPM / BPB.
- Your desired layer's LayerDef sound is valid for that track.
- Zones have collision enabled and overlap events firing.
# 14. Advanced Features (Optional) #ADVANCED
For power users who want to understand internal responsibilities. Most games only need the BFL nodes and profile / zone setup.
Optional: Conductor Function Map (for power users)
| Category | Functions |
|---|---|
| Profile | BST_GetStateConfig, BST_GetStateConfig_FromProfile, BST_PickTrackForState |
| Clock | BST_StartBeatClock, BST_OnBeatTick, BST_OnBar |
| Runtime | BST_SwitchStateNow, BST_CrossfadeBaseTo |
| Public | BST_RequestState |
| State (Internal) | BST_RequestState_Internal |
| Layers | BST_EnsureLayerComponentsPlayingMuted, BST_ApplyEventLayersNow, BST_CurrentTrackHasLayerSound, BST_AddEventLayer |
| Layers (Quantize) | BST_QueueAddLayer, BST_QueueRemoveLayer, BST_QueueClearLayers, BST_ApplyPendingLayerOps_OnBar |
| Playlist | BST_PlayNextTrack_SameState, BST_SetAdvanceLock, BST_ClearAdvanceLock |
| Zones | BST_NotifyZoneEntered, BST_NotifyZoneExited, BST_RecomputeTopZone, BST_ApplyZoneOverrideNow, BST_ResolveDesiredStateAndProfile_Now, BST_ResolveEffectiveStateQuantize |
| Zones (Queued) | BST_RequestRecomputeTopZone, BST_RecomputeTopZone_Queued, BST_ApplyPendingZoneLayers_OnBar |
| State Requests | BST_GetTopStateRequest, BST_ResolveDesiredStateAndProfile_Now, BST_PushStateRequest, BST_RemoveStateRequest, BST_ClearRequests |
| Track Lock | BST_UpdateTrackLockActive, BST_RestartCurrentTrackLocked |
| Intensity | BST_SetIntensity, BST_BeginIntensityTransition, EVT_IntensityRampTick, BST_ResolveIntensityRulesForCurrentTrack, BST_RecomputeActiveIntensityLayers, BST_ApplyIntensityNow_Internal, BST_ApplyPendingIntensity_OnBar, BST_EvalIntensityVolumeScaleForLayer, BST_GetLayerTargetVolume_Now, BST_LayerToBit |
14.2 Intensity System (Advanced)
You can ship your game using only States/Zones/Layers without Intensity. Use Intensity if you want a single gameplay “tension dial” that drives extra layers automatically.
Intensity is a continuous 0..1 value that can automatically enable or disable layers based on a rule list. This gives you a single gameplay “dial” (exploration → tension → combat) without writing multiple Add/Remove calls.
How Intensity Turns Into Layers
The Conductor evaluates the active intensity rules and builds ActiveIntensityLayers. Each rule maps one layer to an “open” threshold (and optionally a separate “close” threshold).
| Rule Field | Meaning |
|---|---|
| Layer | Which layer this rule controls (Drums/Bass/Harmony/Melody/FX). |
| Threshold | Intensity at or above this value will enable the layer. |
| CloseThreshold | Optional: intensity at or below this value will disable the layer (used only when hysteresis is enabled). If left negative (e.g. -1), the Conductor derives it automatically. |
Hysteresis (Why CloseThreshold Exists)
Without hysteresis, if intensity hovers around a threshold (e.g. 0.49 → 0.51 → 0.50), layers may rapidly toggle ON/OFF. Hysteresis adds a “dead band” so a layer opens at one value, but only closes after intensity drops a bit further.
- Open: Intensity ≥ Threshold → layer turns ON
- Close: Intensity ≤ CloseThreshold → layer turns OFF
Drums opens at 0.45. With hysteresis delta 0.10, it closes at 0.35. That means small fluctuations around 0.45 won’t spam toggles.
Hysteresis is a Conductor setting (bUseIntensityHysteresis + DefaultIntensityHysteresisDelta). It is not tied to the Music Profile override mode — you can use hysteresis whether your rules come from the Conductor default, a per-track Profile Override, or a per-track Rules Override.
Quantized vs Immediate Intensity Changes
The gameplay node BFL_BST → BST_SetIntensity includes a Quantize to Next Bar input. When enabled, the request is scheduled and applied cleanly on the next bar tick. When disabled, the new target is applied immediately. Any per-call smoothing overrides (smooth vs instant, and optional ramp seconds) are stored alongside the pending intensity change, so the feel stays consistent even when quantized.
Optional Smoothing / Ramp
You can optionally smooth intensity changes so CurrentIntensity01 ramps toward the target instead of snapping instantly. This is useful when gameplay produces noisy or jumpy values (AI counts, threat meters, distance checks).
- The Conductor has a default smoothing mode (bSmoothIntensity + IntensitySmoothingSeconds).
- BFL_BST → BST_SetIntensity can override smoothing per call (smooth vs instant). This override is respected even when the intensity change is quantized to the next bar.
- If smoothing is active for that call, the Conductor ramps toward the target over IntensitySmoothingSeconds (or your optional per-call ramp seconds override, if you use it).
- Because your thresholds are different per layer, smoothing naturally creates a “build-up” feel: layers can turn on one by one as intensity crosses each threshold.
Smoothing affects when a layer becomes enabled (threshold timing). Layer fade durations affect how the audio volume transitions once enabled. You can use either one alone, but the combo feels most musical.
Min-change Apply (Performance Optimization)
During smoothing, intensity may be evaluated many times per second. However, the set of enabled layers typically changes only when intensity crosses a threshold. BeatSyncToolkit includes a small optimization called Min-change Apply to avoid redundant work while intensity ramps.
- The Conductor computes CurrentIntensityMask (a bitmask) from the rules each time it recomputes ActiveIntensityLayers.
- If CurrentIntensityMask equals LastAppliedIntensityMask, the Conductor skips re-applying event layers for that tick.
- When the mask changes (threshold crossed), it applies once and updates LastAppliedIntensityMask.
This optimization does not change how the music sounds. It only reduces redundant calls to the layer application pipeline. Your audible fades are still controlled by the per-layer fade durations in your Music Profile / TrackEntry layer defs.
How the Bitmask Works (BST_LayerToBit)
To keep the system modular, the Conductor converts a layer enum value into a unique bit using BST_LayerToBit. The default implementation is generic: it returns 2^EnumIndex (equivalent to 1 << EnumIndex). This means you do not need to edit BST_LayerToBit when adding new layers.
Only append new entries to E_BST_MusicLayer. Do not reorder existing entries. Reordering changes enum indices, which changes bit assignments used by CurrentIntensityMask.
Intensity Curves (Volume Scaling)
By default, Intensity is a binary layer gate (a layer is either enabled or not). With curves, an Intensity-enabled layer can also have its target volume scaled as Intensity changes. This enables “gradual build” layers (e.g., drums getting louder) without needing multiple stems.
- Each F_BST_IntensityRule can optionally provide a Volume Scalar Curve (CurveFloat). The curve is evaluated with X = CurrentIntensity01 and outputs a scale value.
- The scale is clamped to 0..1 and applied as: EffectiveTargetVolume = BaseTargetVolume * Scale.
- Important: the curve scaling is applied only when a layer is enabled ONLY by Intensity (Non‑Intensity ON = false, Intensity ON = true). If a layer is enabled by Default/Zone/Event, the system returns BaseTargetVolume unchanged (your normal layer behavior). Also: Intensity does not disable layers that are enabled by Default/Zone/Event — those sources have higher priority, so the layer remains ON even if Intensity drops.
# 15. Known Limitations & Roadmap
15.1 Current Limitations
These are intentional design boundaries or features not yet implemented. None of these prevent shipping a game with BeatSyncToolkit.
| Limitation | Details | Workaround |
|---|---|---|
| No editor preview | Music only plays in PIE or Standalone. There is no "audition in editor" mode. | Use PIE with bStartOnBeginPlay = true for quick testing. |
| 5 fixed layer slots | The built-in layers are Drums, Bass, Harmony, Melody, FX. Adding more requires manual extension (see Section 5.6). | For most games, 5 layers is more than enough. Use States/Zones for additional variation. |
| Single Conductor per level | Only one BP_BST_Conductor should exist at a time. Multiple Conductors are not supported. | Use Zones and State Requests for spatial and event-driven variation within the same Conductor. |
| Timer-based clock (not sample-accurate) | The beat clock uses UE Timers, which are frame-dependent. Very high BPM (300+) or very low frame rates may cause slight timing drift. | For typical game BPM ranges (60–200) at 30+ FPS, timing is solid. Not intended for rhythm-game-level precision. |
| No MIDI input/output | BST does not read or generate MIDI data. | Use gameplay events (BST_SetMusicState, BST_AddLayer) instead of MIDI. |
| No dynamic stem loading | All stems referenced in a profile are loaded when the profile is active. There is no on-demand streaming per stem. | Keep stem file sizes reasonable. Use Sound Cues with streaming settings if memory is a concern. |
15.2 Roadmap
BeatSyncToolkit is designed to be expandable. Upcoming systems are toggles, they will not break the current workflow.
- More convenience BFL nodes (quality-of-life wrappers for common patterns).
- Optional Intensity add-ons (e.g., temporary suppression, per-zone intensity modifiers).
- More demo levels and example profiles.
- Expanded documentation with screenshot walkthroughs.
# 16. Glossary / Key Terms BEGINNER
Quick definitions for common terms used in this documentation.
Separate audio layers of the same music (drums, bass, melody, etc.). Each stem is its own sound file, so BeatSyncToolkit can fade layers in/out independently.
Applying changes on the next bar boundary (instead of instantly). This keeps transitions musically clean.
Beats Per Minute — how fast the music is. BeatSyncToolkit uses BPM to schedule beat/bar timing.
A musical measure. Commonly 4 beats = 1 bar (but it depends on your track’s BeatsPerBar).
Unreal Engine’s sample-accurate audio timing system. BeatSyncToolkit does not use Quartz — it uses a lightweight Conductor timer/clock approach.
Rules for what “wins” when multiple systems try to control music at once (Zones vs Events vs Defaults vs State requests).
A “dead zone” between open/close thresholds so a value doesn’t flicker rapidly when hovering near the threshold.
An Unreal asset used to store structured configuration data. BeatSyncToolkit profiles are Data Assets.
A “grouping key” used by State Requests so multiple instances (like many AI actors) don’t overwrite each other. A scoped request key is typically ScopeKey + RequestName. Use per-actor scope for enemy AI, or use a shared scope for group-wide systems.
A Blueprint reference used to locate the current World. Most of the time, connect Self. Without a valid World context, global BFL nodes may fail to find the placed Conductor.
If any of these terms are unfamiliar, start with Quick Start and come back here later.
# 17. Frequently Asked Questions (FAQ)
Quick answers to the most common questions.
Do I need C++ knowledge?
No — BeatSyncToolkit is Blueprint-first and ships with global Blueprint nodes for runtime control.
Does this use Quartz?
No. BST uses a lightweight timer-based beat clock and bar quantization. Quartz is not required (and BST can coexist with Quartz if your project uses it elsewhere).
Can I add custom Music States?
Yes. Extend E_BST_MusicState, then add tracks for the new state inside your Music Profile DataAsset.
Can I use my own music?
Yes. Any asset playable by an AudioComponent works (Sound Waves, Sound Cues, and MetaSound Source assets). Import your sounds, then assign them in your profile.
Does this work with MetaSounds?
Yes — if you can assign it to an AudioComponent, BST can play it as a base track or as a layer stem.
Is there multiplayer support?
BST is multiplayer-friendly, but audio is typically driven on each client. Replicate your gameplay events (state requests / layers / intensity) to clients, then call the BST nodes locally. Dedicated servers usually do not need to run audio.
Can I preview music in the editor?
BST is designed for runtime playback (PIE / Standalone). It does not provide an 'editor-only audition' mode without pressing Play.
Performance impact?
Very small. At runtime, BeatSyncToolkit uses 2 base AudioComponents (for crossfade) + 5 layer AudioComponents (one per stem) = 7 total AudioComponents. The beat clock is a single lightweight timer. Zone overlap checks use standard UE collision (no tick cost). State/layer arbitration runs only when something changes (event-driven, not per-tick). In practice, BST's CPU footprint is negligible compared to a single particle system. The only scaling concern is if you have a very large number of concurrent State Requests (100+), which is easily solved with a manager pattern (see Section 6).
Roadmap / updates?
Check the Roadmap section for planned additions and version notes.
How do I remove/uninstall BeatSyncToolkit?
Delete the BeatSyncToolkit content folder from your project. Remove any BP_BST_Conductor and BP_BST_MusicZone actors from your levels. Delete or disconnect any BFL_BST node calls in your Blueprints (they will show as broken nodes after removal). No engine files or config files are modified by BST, so removal is clean.
Does it work on UE 5.3 or 5.4?
BeatSyncToolkit is built and tested on UE 5.5.x. It uses only standard Blueprint features, so it is expected to work on 5.3+ without issues. However, only 5.5.x is officially tested and supported. If you encounter issues on an older version, please report them.
Can I use Sound Cues or MetaSound Sources instead of raw WAV?
Yes. Any asset that can be assigned to a standard UE AudioComponent works as a BaseSound or layer stem. This includes SoundWave (.wav, .ogg imports), SoundCue, and MetaSound Source assets.
What happens if I switch states while a crossfade is in progress?
The Conductor handles rapid state switches gracefully. A new state switch during an active crossfade will start a new crossfade from the current mix, so you never hear a "jump". If quantization is enabled, the new switch waits for the next bar regardless.
Can I have different fade speeds per layer?
Yes. Each layer in each TrackEntry has its own FadeInDuration and FadeOutDuration (configured in F_BST_LayerDef). For example, drums can snap in at 0.1s while harmony fades in over 2s.
How do I report issues?
Contact us at nonfigurestudio@gmail.com and include your engine version, a short repro checklist, and screenshots if possible.
# 18. Support & Contact
Need help or want to report an issue? Reach out to us.
Email Support
For questions, bug reports, or feature requests, contact us at:
- Your Unreal Engine version (e.g., UE 5.5.2)
- Steps to reproduce the issue
- Screenshots or error messages if applicable
- Brief description of your setup (project type, music profile details)