Blueprint Sound Node: Cue Player
Overview Author: () 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 an...
Overview
Author: ()
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:
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 SoundCueAssetPtr;
UPROPERTY(transient)
USoundCue* SoundCue;
// Make sure Cue Player doesn't play the same Cue we created
TSoftObjectPtr 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& 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, TArrayDuration;
}
#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.
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!