Hey, I’m trying to play a random song in my game’s soundtrack by using a random node inside a sound cue, but in a packaged game, it starts off with the same song every time.

Hey, I’m trying to play a random song in my game’s soundtrack by using a random node inside a sound cue, but in a packaged game, it starts off with the same song every time.

bump
I’m not sure what the “correct” way to do this is (it might just be a checkbox somewhere), but you could try using the switch node in the sound cue and setting it randomly in blueprint.
This node:

Scroll down this page to learn how to use.
I ended up creating a cpp actor to acheive a random music playlist:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MusicPlaylist.generated.h"
UCLASS()
class STARSURFER_API AMusicPlaylist : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMusicPlaylist();
void ResetQueue();
void PlayNextSong();
void ShuffleArray(TArray<class USoundCue*>& myArray);
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Music")
TArray<USoundCue*> Playlist;
TQueue<USoundCue*> PlaylistQueue;
float SongTimer = 0;
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
};
#include "MusicPlaylist.h"
#include "Sound/SoundCue.h"
#include "Kismet/GameplayStatics.h"
// Sets default values
AMusicPlaylist::AMusicPlaylist()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bTickEvenWhenPaused = true;
}
void AMusicPlaylist::ResetQueue()
{
PlaylistQueue.Empty();
ShuffleArray(Playlist);
for (USoundCue* Cue : Playlist)
{
PlaylistQueue.Enqueue(Cue);
}
}
void AMusicPlaylist::PlayNextSong()
{
USoundCue* Sound = *PlaylistQueue.Peek();
SongTimer = Sound->Duration;
UGameplayStatics::PlaySound2D(GWorld, Sound);
PlaylistQueue.Pop();
}
// Called when the game starts or when spawned
void AMusicPlaylist::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void AMusicPlaylist::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
SongTimer-=DeltaTime;
if (SongTimer < 0.f)
{
if (PlaylistQueue.IsEmpty())
{
ResetQueue();
}
PlayNextSong();
}
}
void AMusicPlaylist::ShuffleArray(TArray<USoundCue*>& myArray)
{
if (myArray.Num() > 0)
{
int32 LastIndex = myArray.Num() - 1;
for (int32 i = 0; i <= LastIndex; ++i)
{
int32 Index = FMath::RandRange(i, LastIndex);
if (i != Index)
{
myArray.Swap(i, Index);
}
}
}
}
For anyone else that reaches this page wondering about the random node not working. As Detach789 mentioned you can achieve the same results with the switch node. It just requires a little more setup. It works in both blueprint and C++.
First create your sound cue with the switch.
Then in either blueprint or c++ execute some logic on the audio player such as an AudioComponent.
Blueprint:
C++
Include module “AudioExtensions” in your project.build.cs file inside the PublicDependencyModuleNames.AddRange sub section.
Include this header in your .cpp file:
#include "AudioParameterControllerInterface.h"
Then in your code get a reference to the interface from the audio component to reference SetIntParameter().
It will look like this:
if (!MenuMusic)
{
return;
}
AudioComponent->SetSound(MenuMusic);
if (IAudioParameterControllerInterface* Interface = Cast<IAudioParameterControllerInterface>(AudioComponent))
{
Interface->SetIntParameter("MenuMusic", FMath::RandRange(0, 18));
}
AudioComponent->Play(0.0f);
Just be very careful with the RandRange function. Be sure not to put values that are outside the range of the switch in your cue.
** Note **
After testing this, I found that if you are using a looper node in the sound cue it will simply loop the same index in the switch over and over. So, in this case you want to remove the looper and bind an event from the audio component called OnAudioFinished()
This can be done in both blueprint and c++. The blueprint way is the normal way you bind regular delegates.
In c++ it looks like this:
In your class header file, create a UFUNCTION() function that will bind to the audio component delegate:
.h
UFUNCTION()
void OnAudioFinished()
In your cpp file add the below logic to begin play and the logic for changing the switch parameter and playing the audio component again inside your new bound function:
.cpp
void AYourActor::BeginPlay()
{
AudioComponent->OnAudioFinished.AddDynamic(this, &AYourActor::OnAudioFinished();
}
void AYourActor::OnAudioFinished()
{
if (IAudioParameterControllerInterface* Interface = Cast<IAudioParameterControllerInterface>(AudioComponent))
{
if (bIsMainMenuActive)
{
Interface->SetIntParameter("MenuMusic", FMath::RandRange(MenuAudioIndexMin, MenuAudioIndexMax));
AudioComponent->Play(0.0f);
}
else
{
Interface->SetIntParameter("GameplayMusic", FMath::RandRange(GameplayAudioIndexMin, GameplayAudioIndexMax));
AudioComponent->Play(0.0f);
}
}
}
In my code I even made a few UPROPERTY variables in my player controller that I use to define the min and max values of sound cue waves.
Seems to be a bug. Im using UE5.6. Following code creates a new Random node that can be used in the Cue:
#pragma once
include “CoreMinimal.h”
include “Sound/SoundNode.h”
include “SoundNodeTrueRandom.generated.h”
class FAudioDevice;
struct FActiveSound;
struct FSoundParseParameters;
struct FWaveInstance;
UCLASS(hidecategories=Object, editinlinenew, meta=(DisplayName=“True Random”))
class BLA_API USoundNodeTrueRandom : public USoundNode
{
GENERATED_BODY()public:
USoundNodeTrueRandom(); UPROPERTY(EditAnywhere, editfixedsize, Category=Random) TArray<float> Weights; UPROPERTY(Transient) TArray<bool> HasBeenUsed; UPROPERTY(Transient) int32 NumRandomUsed; UPROPERTY(EditAnywhere, Category=Random) uint8 bRandomizeWithoutReplacement : 1; virtual void PostLoad() override; virtual void PostEditImport() override; virtual void ParseNodes(FAudioDevice\* AudioDevice, const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound, const FSoundParseParameters& ParseParams, TArray<FWaveInstance\*>& WaveInstances) override; virtual int32 GetNumSounds(const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound) const override; virtual int32 GetMaxChildNodes() const override; virtual void InsertChildNode(int32 Index) override; virtual void RemoveChildNode(int32 Index) override;#if WITH_EDITOR
virtual void SetChildNodes(TArray<USoundNode\*>& InChildNodes) override;#endif
virtual void CreateStartingConnectors() override; void FixWeightsArray(); void FixHasBeenUsedArray(); int32 ChooseNodeIndex(const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound);private:
void ReseedRandomStream(); int32 GetNumSelectableChildren() const;};
CPP:
include “SoundNodeTrueRandom.h”
include “ActiveSound.h”
include “HAL/PlatformTime.h”
USoundNodeTrueRandom::USoundNodeTrueRandom()
: NumRandomUsed(0) , bRandomizeWithoutReplacement(false){
ReseedRandomStream();}
void USoundNodeTrueRandom::PostLoad()
{
Super::PostLoad(); FixWeightsArray(); FixHasBeenUsedArray(); NumRandomUsed = 0; ReseedRandomStream();}
void USoundNodeTrueRandom::PostEditImport()
{
Super::PostEditImport(); FixWeightsArray(); FixHasBeenUsedArray(); NumRandomUsed = 0; ReseedRandomStream();}
void USoundNodeTrueRandom::FixWeightsArray()
{
if (Weights.Num() < ChildNodes.Num()) { const int32 OldNum = Weights.Num(); Weights.AddZeroed(ChildNodes.Num() - Weights.Num()); for (int32 Index = OldNum; Index < Weights.Num(); ++Index) { Weights\[Index\] = 1.0f; } } else if (Weights.Num() > ChildNodes.Num()) { const int32 NumToRemove = Weights.Num() - ChildNodes.Num(); Weights.RemoveAt(Weights.Num() - NumToRemove, NumToRemove); }}
void USoundNodeTrueRandom::FixHasBeenUsedArray()
{
if (HasBeenUsed.Num() < ChildNodes.Num()) { HasBeenUsed.AddZeroed(ChildNodes.Num() - HasBeenUsed.Num()); } else if (HasBeenUsed.Num() > ChildNodes.Num()) { const int32 NumToRemove = HasBeenUsed.Num() - ChildNodes.Num(); HasBeenUsed.RemoveAt(HasBeenUsed.Num() - NumToRemove, NumToRemove); } NumRandomUsed = 0; for (const bool bUsed : HasBeenUsed) { if (bUsed) { ++NumRandomUsed; } }}
int32 USoundNodeTrueRandom::GetNumSelectableChildren() const
{
return FMath::Min(ChildNodes.Num(), Weights.Num());}
int32 USoundNodeTrueRandom::ChooseNodeIndex(const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound)
{
const int32 NumSelectableChildren = GetNumSelectableChildren(); if (NumSelectableChildren <= 0) { return INDEX_NONE; } TArray<int32, TInlineAllocator<USoundNode::MAX_ALLOWED_CHILD_NODES>> AvailableIndices; AvailableIndices.Reserve(NumSelectableChildren); float WeightSum = 0.0f; for (int32 Index = 0; Index < NumSelectableChildren; ++Index) { if (bRandomizeWithoutReplacement && HasBeenUsed.IsValidIndex(Index) && HasBeenUsed\[Index\]) { continue; } AvailableIndices.Add(Index); WeightSum += FMath::Max(0.0f, Weights\[Index\]); } if (AvailableIndices.Num() == 0) { for (int32 Index = 0; Index < NumSelectableChildren; ++Index) { if (HasBeenUsed.IsValidIndex(Index)) { HasBeenUsed\[Index\] = false; } } NumRandomUsed = 0; for (int32 Index = 0; Index < NumSelectableChildren; ++Index) { AvailableIndices.Add(Index); WeightSum += FMath::Max(0.0f, Weights\[Index\]); } } const uint32 Mix = static_cast<uint32>(FPlatformTime::Cycles64() ^ static_cast<uint64>(NodeWaveInstanceHash) ^ reinterpret_cast<UPTRINT>(&ActiveSound)); RandomStream.Initialize(RandomStream.GetCurrentSeed() ^ Mix); int32 SelectedIndex = AvailableIndices\[0\]; if (WeightSum > 0.0f) { const float Choice = RandomStream.GetFraction() \* WeightSum; float RunningWeight = 0.0f; for (const int32 CandidateIndex : AvailableIndices) { RunningWeight += FMath::Max(0.0f, Weights\[CandidateIndex\]); if (Choice < RunningWeight) { SelectedIndex = CandidateIndex; break; } } } else { SelectedIndex = AvailableIndices\[RandomStream.RandRange(0, AvailableIndices.Num() - 1)\]; } if (HasBeenUsed.IsValidIndex(SelectedIndex)) { if (!HasBeenUsed\[SelectedIndex\]) { HasBeenUsed\[SelectedIndex\] = true; ++NumRandomUsed; } } if (bRandomizeWithoutReplacement && NumRandomUsed >= NumSelectableChildren) { for (int32 Index = 0; Index < NumSelectableChildren; ++Index) { if (HasBeenUsed.IsValidIndex(Index)) { HasBeenUsed\[Index\] = false; } } if (HasBeenUsed.IsValidIndex(SelectedIndex)) { HasBeenUsed\[SelectedIndex\] = true; } NumRandomUsed = 1; } return SelectedIndex;}
void USoundNodeTrueRandom::ParseNodes(FAudioDevice* AudioDevice, const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound, const FSoundParseParameters& ParseParams, TArray<FWaveInstance*>& WaveInstances)
{
RETRIEVE_SOUNDNODE_PAYLOAD(sizeof(int32)); DECLARE_SOUNDNODE_ELEMENT(int32, NodeIndex); if (\*RequiresInitialization) { NodeIndex = ChooseNodeIndex(NodeWaveInstanceHash, ActiveSound); \*RequiresInitialization = 0; } if (ChildNodes.IsValidIndex(NodeIndex) && ChildNodes\[NodeIndex\]) { ChildNodes\[NodeIndex\]->ParseNodes(AudioDevice, GetNodeWaveInstanceHash(NodeWaveInstanceHash, ChildNodes\[NodeIndex\], NodeIndex), ActiveSound, ParseParams, WaveInstances); }}
int32 USoundNodeTrueRandom::GetNumSounds(const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound) const
{
RETRIEVE_SOUNDNODE_PAYLOAD(sizeof(int32)); DECLARE_SOUNDNODE_ELEMENT(int32, NodeIndex); if (\*RequiresInitialization) { NodeIndex = const_cast<USoundNodeTrueRandom\*>(this)->ChooseNodeIndex(NodeWaveInstanceHash, ActiveSound); \*RequiresInitialization = 0; } if (ChildNodes.IsValidIndex(NodeIndex) && ChildNodes\[NodeIndex\]) { const UPTRINT ChildNodeWaveInstanceHash = GetNodeWaveInstanceHash(NodeWaveInstanceHash, ChildNodes\[NodeIndex\], NodeIndex); return ChildNodes\[NodeIndex\]->GetNumSounds(ChildNodeWaveInstanceHash, ActiveSound); } return 0;}
int32 USoundNodeTrueRandom::GetMaxChildNodes() const
{
return MAX_ALLOWED_CHILD_NODES;}
void USoundNodeTrueRandom::InsertChildNode(int32 Index)
{
FixWeightsArray(); FixHasBeenUsedArray(); Weights.Insert(1.0f, Index); HasBeenUsed.Insert(false, Index); NumRandomUsed = 0; Super::InsertChildNode(Index);}
void USoundNodeTrueRandom::RemoveChildNode(int32 Index)
{
FixWeightsArray(); FixHasBeenUsedArray(); Weights.RemoveAt(Index); HasBeenUsed.RemoveAt(Index); NumRandomUsed = 0; Super::RemoveChildNode(Index);}
#if WITH_EDITOR
void USoundNodeTrueRandom::SetChildNodes(TArray<USoundNode*>& InChildNodes)
{
Super::SetChildNodes(InChildNodes); FixWeightsArray(); FixHasBeenUsedArray(); for (int32 Index = 0; Index < Weights.Num(); ++Index) { if (Weights\[Index\] <= 0.0f) { Weights\[Index\] = 1.0f; } }}
#endif
void USoundNodeTrueRandom::CreateStartingConnectors()
{
InsertChildNode(ChildNodes.Num()); InsertChildNode(ChildNodes.Num());}
void USoundNodeTrueRandom::ReseedRandomStream()
{
const uint32 Seed = static_cast<uint32>(FPlatformTime::Cycles64() ^ reinterpret_cast<UPTRINT>(this)); RandomStream.Initialize(Seed);}