Vertical Re-Mixing in Unreal Engine with Metasounds + Quartz (Part 2/2)


Si prefieres leer una versión en Español, da click en este botón =>

Unreal Engine Version: 5.0.3

This blog post shows how to implement a simple looping music cue using Unreal Engine's Metasounds. This music cue has four “layers” that play interactively and in sync, one on top of the other, to create a vertical re-mixing system. Finally, I show how to trigger changes or states on this metasound using a Quartz clock.

I implemented the same systems twice for this test project, one using Blueprints visual scripting and another using native C++. Here is the list of essential tools and keywords used for this project:

I programmed all the functionality inside the MusicPlayerActor C++ parent class, BP_MusicPlayerActor blueprint class, and the ThirdPersonMap level blueprint.

Important:

Please consider that this guide does not intend to teach creative sound design. Instead, it exclusively focuses on game audio's implementation and programming aspects.


DOWNLOAD

〰️

DOWNLOAD 〰️

Download the project here:

DOWNLOAD

〰️

DOWNLOAD 〰️


 

Part 2 - Quartz:

Dependencies:

To use the Quartz subsystem and declare Metasound properties add the “AudioMixer” and “MetasoundEngine” modules to the Build.cs file:

// Add the "MetasoundEngine" module to use Metasound Source.
// Add the "AudioMixer" module to use the Quartz Subsystem. 
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "MetasoundEngine", "AudioMixer"});
 

MusicPlayerActor directives and property declarations:

Use these directives in the MusicPlayerActor.h file to use MetasoundSource and the Quartz Clock Handle. Please read the code's comments for details:

#include "Quartz/AudioMixerClockHandle.h" //Include this directive to access the Quartz clock.
#include "MetasoundSource.h" //Include this directive to use MetasoundSource properties.

I declared these properties in the MusicPlayerActor.h file. Please read the code's comments for details:

/** Enables on screen messages for debug. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug")
bool bDebug = false;

/** Metasound music cue. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Music")
UMetaSoundSource* MusicCue;

/** Audio component that will hold and control the Metasound music cue. */
UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Music")
UAudioComponent* MusicAudioComponent;

/** Current trigger parameter name. It changes and updates the music "state" on the Metasound. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Music")
FName TriggerParameter;

/** Quartz clock. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock")
UQuartzClockHandle* MusicClock;

/** Name for the Quartz music clock. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock")
FName NewClockName = "MusicClock";

/** Music meter numerator. Default value = 4. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock")
int MeterNumerator = 4;

/** Music meter denominator enumeration. Default value = QuarterNote. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock")
EQuartzTimeSignatureQuantization MeterDenominator = EQuartzTimeSignatureQuantization::QuarterNote;

/** Beats per minute to set for the Quartz clock. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock")
float BPM = 76.0f;

/** Number of bars for the MusicCue loop. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock")
int BarNumbers = 8;

/** Defines the musical timing boundary when the functions will trigger.
 *  As default, this enumeration will trigger the fun ctions at the end of a half note = 1/2. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock")
EQuartzCommandQuantization QuantizationBoundary = EQuartzCommandQuantization::HalfNote;
 

MusicPlayerActor function and delegate declarations:

I declared these functions and function delegates in the MusicPlayerActor.h file. Please read the code's comments for the details:

/**
 * @brief Sets and creates a new Quartz clock and starts the music cue quantized.
 * @param ClockName Name for the new Quartz clock.
 * @param NumBeats Meter numerator.
 * @param BeatType Meter denominator.
 * @param BeatsPerMinute Beats per minute / tempo.
 * @param AudioComponent Music audio component.
 */
UFUNCTION(BlueprintCallable, Category="Music")
void CreateClockAndPlayMusic(FName ClockName, int NumBeats, EQuartzTimeSignatureQuantization BeatType, float BeatsPerMinute, UAudioComponent* AudioComponent);

/**
 * @brief Function delegate. Triggers its functionality synced with the clock.
 * @param EventType Use a switch on this enumeration to select "CommandOnQueued". 
 * @param Name 
 */
UFUNCTION()
void FPlayQuantizedDelegate(EQuartzCommandDelegateSubType EventType, FName Name);

/** Used to bind the FPlayQuantizedDelegate in the class constructor. */
FOnQuartzCommandEventBP PlayQuantizationDelegate;

	
/**
 * @brief Executes a trigger parameter synced with the clock.
 * @param Clock Quartz clock.
 * @param InQuantizationBoundary Quantization boundary.
 * @param ParameterName Parameter to be triggered on the Music Cue.
 */
UFUNCTION(BlueprintCallable, Category="Music")
void ExecuteTriggerInTime(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, FName ParameterName);

/**
 * @brief Function delegate. Triggers its functionality synced with the clock.
 * @param ClockName Name of the Quartz clock.
 * @param QuantizationType 
 * @param NumBars 
 * @param Beat 
 * @param BeatFraction 
 */
UFUNCTION()
void FExecuteTriggerDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction);

/** Used to bind the FPlayQuantizedDelegate in the class constructor. */
FOnQuartzMetronomeEventBP ExecuteTriggerDelegate;

	
/**
 * @brief Resets the clock at the last bar of the music loop.
 * @param Clock Quartz clock.
 * @param InQuantizationBoundary Quantization boundary.
 * @param NumBars Number of bars for the MusicCue loop.
 */
UFUNCTION(BlueprintCallable, Category="Music")
void UpdateAndResetClock(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, int NumBars);

/**
 * @brief Function delegate. Triggers its functionality synced with the clock.
 * @param ClockName Name of the Quartz clock.
 * @param QuantizationType 
 * @param NumBars 
 * @param Beat 
 * @param BeatFraction 
 */
UFUNCTION()
void FUpdateClockDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction);

/** Used to bind the FPlayQuantizedDelegate in the class constructor. */
FOnQuartzMetronomeEventBP UpdateClockDelegate;
 

C++ and Blueprint implementation:

I created a blueprint class derived from MusicPlayerActor, called BP_MusicPlayerActor. In this section, I show this class’ function definitions both in C++ and in the Blueprint Graph.

The Constructor:

All the functions in this class require using function delegates that will trigger based on musical time. Function delegates are bound to UFunctions inside the class constructor. Additionally, I created and registered the Audio Component that holds the music segment.

MusicPlayerActor.cpp:

#include "Components/AudioComponent.h" //Include this directive to use AudioComponents.

// Sets default values
AMusicPlayerActor::AMusicPlayerActor()
{
	/** Creates an audio component. */
	MusicAudioComponent = CreateDefaultSubobject<UAudioComponent>(TEXT("Audio Component"));
	MusicAudioComponent->SetAutoActivate(false);
	MusicAudioComponent->SetSound(MusicCue);

	/** Binds all the function delegates. */
	PlayQuantizationDelegate.BindUFunction(this, "FPlayQuantizedDelegate");
	ExecuteTriggerDelegate.BindUFunction(this, "FExecuteTriggerDelegate");
	UpdateClockDelegate.BindUFunction(this, "FUpdateClockDelegate");

	
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;
}

Create Clock And Play Music:

Sets and creates a new Quartz clock and starts the music cue quantized.

MusicPlayerActor.cpp:

void AMusicPlayerActor::CreateClockAndPlayMusic(FName ClockName, int NumBeats, EQuartzTimeSignatureQuantization BeatType, float BeatsPerMinute, UAudioComponent* AudioComponent)
{
	/** Gets a reference from the Quartz subsystem from the world. */
	UQuartzSubsystem* Quartz = GetWorld()->GetSubsystem<UQuartzSubsystem>();

	/** Defines settings for FQuartzTimeSignature and FQuartzClockSettings structures. */
	FQuartzTimeSignature TimeSignature;
	TimeSignature.NumBeats = NumBeats;
	TimeSignature.BeatType = BeatType;
	FQuartzClockSettings ClockSettings;
	ClockSettings.TimeSignature = TimeSignature;

	/** Creates a new clock the previous setting structures. */
	MusicClock = Quartz->CreateNewClock(this, ClockName, ClockSettings, true);
	/** Sets the tempo for the clock. */
	MusicClock->SetBeatsPerMinute(this, FQuartzQuantizationBoundary(), FOnQuartzCommandEventBP(), MusicClock, BeatsPerMinute);

	/** Defines settings for the FQuartzQuantizationBoundary structure. */
	FQuartzQuantizationBoundary QuartzQuantizationBoundary;
	QuartzQuantizationBoundary.Quantization = EQuartzCommandQuantization::Bar;
	QuartzQuantizationBoundary.Multiplier = 1.0f;
	QuartzQuantizationBoundary.CountingReferencePoint = EQuarztQuantizationReference::BarRelative;
	QuartzQuantizationBoundary.bResetClockOnQueued = true;

	/** Plays the music cue from the audio component after the audio is "Queued" and loaded. Starts the clock. */
	AudioComponent->PlayQuantized(this, MusicClock, QuartzQuantizationBoundary, PlayQuantizationDelegate);
}

void AMusicPlayerActor::FPlayQuantizedDelegate(EQuartzCommandDelegateSubType EventType, FName Name)
{
	/** Breaks this enumeration on different execution paths. */
	switch (EventType)
	{
	/** Starts the clock after the audio cue is ready or "Queued" */
	case EQuartzCommandDelegateSubType::CommandOnQueued:
		MusicClock->StartClock(this, MusicClock);
		break;
		
	default:
		break;
	}

	if(bDebug) /** if bDebug is true, adds these messages to the screen. */
	{
		GEngine->AddOnScreenDebugMessage(3, 5.0f, FColor::Green, "Music Start");
		GEngine->AddOnScreenDebugMessage(4, 5.0f, FColor::Red, "C++ Implementation");
	}
}

BP_MusicPlayerActor:

 

Execute Trigger In Time:

Executes a trigger parameter synced with the clock.

MusicPlayerActor.cpp:

void AMusicPlayerActor::ExecuteTriggerInTime(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, FName ParameterName)
{
	/** Gets a copy of the new trigger parameter. */
	TriggerParameter = ParameterName;
	
	/** Creates a delegate that will trigger synced with the clock. */
	Clock->SubscribeToQuantizationEvent(this, InQuantizationBoundary, ExecuteTriggerDelegate, Clock);
}

void AMusicPlayerActor::FExecuteTriggerDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction)
{
	/** Sets the new parameter in time. */
	MusicAudioComponent->SetTriggerParameter(TriggerParameter);
	/** Unsubscribe from the delegate so the parameter trigger once. */
	MusicClock->UnsubscribeFromTimeDivision(this, QuantizationBoundary, MusicClock);

	if (bDebug) /** if bDebug is true, adds these messages to the screen. */
	{
		GEngine->AddOnScreenDebugMessage(3, 5.0f, FColor::Magenta, TriggerParameter.ToString());
		GEngine->AddOnScreenDebugMessage(4, 5.0f, FColor::Red, "C++ Implementation");
	}
}

BP_MusicPlayerActor:

 

Update And Reset Clock:

Resets the clock at the last bar of the music loop.

MusicPlayerActor.cpp:

void AMusicPlayerActor::UpdateAndResetClock(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, int NumBars)
{
	/** Creates a delegate that will trigger synced with the clock. */
	Clock->SubscribeToQuantizationEvent(this, EQuartzCommandQuantization::EighthNote, UpdateClockDelegate, Clock);
}

void AMusicPlayerActor::FUpdateClockDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction)
{
	/** Defines settings for the FQuartzQuantizationBoundary structure. */
	FQuartzQuantizationBoundary QuartzQuantizationBoundary;
	QuartzQuantizationBoundary.Quantization = QuantizationBoundary;
	QuartzQuantizationBoundary.Multiplier = 1.0f;
	QuartzQuantizationBoundary.CountingReferencePoint = EQuarztQuantizationReference::BarRelative;

	/** If the current bar position is equal to the music cue lenght, reset the clock at the end of that boundary.  */
	if(NumBars == BarNumbers)
		MusicClock->ResetTransportQuantized(this, QuantizationBoundary, FOnQuartzCommandEventBP(), MusicClock);

	/** if bDebug is true, adds these messages to the screen. */
	if(bDebug)
	{
		GEngine->AddOnScreenDebugMessage(1, 1.0f, FColor::Orange, "Bar: " + FString::FromInt(NumBars));
		GEngine->AddOnScreenDebugMessage(2, 1.0f, FColor::Purple, "Beat: " + FString::FromInt(Beat));
	}
}

BP_MusicPlayerActor:

Full Cpp implementation:

 

The Level Blueprint:

For this project, I created four trigger volumes that call the functions from BP_MusicPlayerActor when the Player Character overlaps with them. Here I show how I implemented the triggering system that starts and changes the music mix. To open the level blueprint click on the blueprint class icon on top of the viewport.

The Viewport:

Level Blueprint Implementation:

Events:

Click on the images to open them.

 

THE END

〰️〰️〰️

THE END 〰️〰️〰️

Previous
Previous

How To Create an Audio Manager in Unreal Engine

Next
Next

Vertical Re-Mixing in Unreal Engine with Metasounds + Quartz (Part 1/2)