Blueprint Sound Node: Cue Player

From Epic Wiki
Jump to: navigation, search



Author: Ursu (talk)

Dear Community,
In this tutorial I want to show you a custom Sound Node which can be used in a Sound Cue Editor. By default, Sound Cue Editor allows you to import Sound Waves and edit them by using various Sound Nodes such as Mixer, Modulator, Random etc. However, sometimes you may want to combine many Sound Cues together or a Sound Cue with a Sound Wave. This is why I’ve decided to follow in Tom Looman's footsteps and create a custom Sound Node called CuePlayer.


In your project, create a new C++ class which inherits from SoundNodeAssetReferencer. In a Choose Parent Class window, check the Show All Classes checkbox and type SoundNodeAssetReferencer. Then click 'Next' and name it 'SoundNodeCuePlayer'.


SoundNodeCuePlayer will be pretty similar to the SoundNodeWavePlayer class (both of them inherit from SoundNodeAssetReferencer). Here is how the header file should look like:


<syntaxhighlight lang="cpp">

  1. pragma once
  1. include "Sound/SoundNodeAssetReferencer.h"
  2. include "SoundNodeCuePlayer.generated.h"

class USoundCue;


  • Sound node that contains a reference to the Sound Cue file to be played
  • /

UCLASS(hidecategories = Object, editinlinenew, meta = (DisplayName = "Cue Player")) class CUSTOMAUDIOPROJECT_API USoundNodeCuePlayer : public USoundNodeAssetReferencer { // IMPORTANT: Please remember to update the '*_API' identifier above to match your own project GENERATED_BODY()

private: UPROPERTY(EditAnywhere, Category = CuePlayer, meta = (DisplayName = "Sound Cue")) TSoftObjectPtr<USoundCue> SoundCueAssetPtr;

UPROPERTY(transient) USoundCue* SoundCue;

// Make sure Cue Player doesn't play the same Cue we created TSoftObjectPtr<USoundNodeCuePlayer> CuePlayerAssetPtr = this; bool IsTheSameSoundCue();

void OnSoundCueLoaded(const FName& PackageName, UPackage* Package, EAsyncLoadingResult::Type Result, bool bAddToRoot);

uint32 bAsyncLoading : 1;

public: //~ Begin UObject Interface virtual void Serialize(FArchive& Ar) override;


virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;

  1. endif

//~ End UObject Interface

//~ Begin USoundNode Interface virtual int32 GetMaxChildNodes() const { return 0; } // A Cue Player is the end of the chain, so it has no children virtual float GetDuration() override; virtual void ParseNodes(FAudioDevice* AudioDevice, const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound, const FSoundParseParameters& ParseParams, TArray<FWaveInstance*>& WaveInstances) override;


virtual FText GetTitle() const override;

  1. endif

//~ End USoundNode Interface

//~ Begin USoundNodeAssetReferencer Interface virtual void LoadAsset(bool bAddToRoot = false) override; virtual void ClearAssetReferences() override; //~ End USoundNode Interface



IMPORTANT: Please remember to change the '*_API' identifier, because it has to match your project's name. My project was called CustomAudioProject, this is why there is a 'CUSTOMAUDIOPROJECT_API' identifier in the header's code.


<syntaxhighlight lang="cpp">

  1. include "SoundNodeCuePlayer.h"
  2. include "ActiveSound.h"
  3. include "Sound/SoundCue.h"
  4. include "FrameworkObjectVersion.h"
  1. define LOCTEXT_NAMESPACE "SoundNodeCuePlayer"

void USoundNodeCuePlayer::Serialize(FArchive& Ar) { Super::Serialize(Ar);


if (Ar.CustomVer(FFrameworkObjectVersion::GUID) >= FFrameworkObjectVersion::HardSoundReferences) { if (Ar.IsLoading()) Ar << SoundCue; else if (Ar.IsSaving()) { USoundCue* HardReference = (ShouldHardReferenceAsset() ? SoundCue : nullptr); Ar << HardReference; } } }

void USoundNodeCuePlayer::LoadAsset(bool bAddToRoot) { if (IsAsyncLoading()) { SoundCue = SoundCueAssetPtr.Get(); if (!SoundCue) { const FString LongPackageName = SoundCueAssetPtr.GetLongPackageName(); if (!LongPackageName.IsEmpty()) { bAsyncLoading = true; LoadPackageAsync(LongPackageName, FLoadPackageAsyncDelegate::CreateUObject(this, &USoundNodeCuePlayer::OnSoundCueLoaded, bAddToRoot)); } } else if (bAddToRoot) SoundCue->AddToRoot(); if (SoundCue) SoundCue->AddToCluster(this); } else { SoundCue = SoundCueAssetPtr.LoadSynchronous(); if (SoundCue) { if (bAddToRoot) SoundCue->AddToRoot(); SoundCue->AddToCluster(this); } } }

void USoundNodeCuePlayer::ClearAssetReferences() { SoundCue = nullptr; }

void USoundNodeCuePlayer::OnSoundCueLoaded(const FName& PackageName, UPackage* Package, EAsyncLoadingResult::Type Result, bool bAddToRoot) { if (Result == EAsyncLoadingResult::Succeeded) { SoundCue = SoundCueAssetPtr.Get(); if (SoundCue) { if (bAddToRoot) SoundCue->AddToRoot(); SoundCue->AddToCluster(this); } } bAsyncLoading = false; }


void USoundNodeCuePlayer::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { if (PropertyChangedEvent.Property && PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(USoundNodeCuePlayer, SoundCueAssetPtr)) LoadAsset(); }

  1. endif

void USoundNodeCuePlayer::ParseNodes(FAudioDevice* AudioDevice, const UPTRINT NodeWaveInstanceHash, FActiveSound& ActiveSound, const FSoundParseParameters& ParseParams, TArray<FWaveInstance*>& WaveInstances) { if (bAsyncLoading) { UE_LOG(LogAudio, Verbose, TEXT("Asynchronous load of %s not complete in USoundNodeCuePlayer::ParseNodes, will attempt to play later."), *GetFullNameSafe(this)); // We're still loading so don't stop this active sound yet ActiveSound.bFinished = false; return; }

if (SoundCue && !IsTheSameSoundCue()) SoundCue->Parse(AudioDevice, NodeWaveInstanceHash, ActiveSound, ParseParams, WaveInstances); }

float USoundNodeCuePlayer::GetDuration() { return SoundCue->Duration; }


FText USoundNodeCuePlayer::GetTitle() const { FText SoundCueName; if (SoundCue) SoundCueName = FText::FromString(SoundCue->GetFName().ToString()); else SoundCueName = LOCTEXT("NoSoundCue", "NONE");

FText Title;

FFormatNamedArguments Arguments; Arguments.Add(TEXT("Description"), Super::GetTitle()); Arguments.Add(TEXT("SoundCueName"), SoundCueName); Title = FText::Format(LOCTEXT("SoundCueDescription", "{Description} : {SoundCueName}"), Arguments);

return Title; }

  1. endif

bool USoundNodeCuePlayer::IsTheSameSoundCue() { if (SoundCueAssetPtr) return SoundCueAssetPtr.GetAssetName() == CuePlayerAssetPtr.GetAssetName(); return false; }



IMPORTANT: Again, that source file is similar to SoundNodeWavePlayer.cpp. However, the IsTheSameSoundCue() function is actually pretty important. As I said before: by default Sound Cue Editor allows you to import Sound Waves only. We want to create a Cue Player node, so we have to be sure that Cue Player node won't play the Sound Cue we're currently editing in Sound Cue Editor.


So, if the asset in the Cue Player node is the Sound Cue we're currently working on, we don't want to parse the sound and generate WaveInstances to play. This is why the IsTheSameSoundCue() function is called inside ParseNodes().


If you changed the '*_API' identifier in SoundNodeCuePlayer.h and successfully build your solution, you should be able to use the Cue Player node in Sound Cue Editor. Now you can mix Sound Cues and Sound Waves inside a single Sound Cue. Yay!