Blueprint Sound Node: Cue Player

Rate this Article:
0.00

Approved for Versions:4.18

Overview

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.

Mixer.PNG

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'.

ParentClassCuePlayer.PNG

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

SoundNodeCuePlayer.h

#pragma once
 
#include "Sound/SoundNodeAssetReferencer.h"
#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;
#if WITH_EDITOR
	virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#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;
#if WITH_EDITOR
	virtual FText GetTitle() const override;
#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.

SoundNodeCuePlayer.cpp

#include "SoundNodeCuePlayer.h"
#include "ActiveSound.h"
#include "Sound/SoundCue.h"
#include "FrameworkObjectVersion.h"
 
#define LOCTEXT_NAMESPACE "SoundNodeCuePlayer"
 
void USoundNodeCuePlayer::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);
 
	Ar.UsingCustomVersion(FFrameworkObjectVersion::GUID);
 
	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;
}
 
#if WITH_EDITOR
void USoundNodeCuePlayer::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	if (PropertyChangedEvent.Property && PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(USoundNodeCuePlayer, SoundCueAssetPtr))
		LoadAsset();
}
#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;
}
 
#if WITH_EDITOR
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;
}
#endif
 
bool USoundNodeCuePlayer::IsTheSameSoundCue()
{
	if (SoundCueAssetPtr)
		return SoundCueAssetPtr.GetAssetName() == CuePlayerAssetPtr.GetAssetName();
	return false;
}
 
#undef LOCTEXT_NAMESPACE

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.

1z76s1.jpg

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().

Results

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!

1z6ayo.jpg