Re-Mezcla Vertical en Unreal Engine con Metasounds + Quartz (Parte 2/2)
If you prefer to read an English version, click on this button =>
Unreal Engine Version: 5.0.3
Este blog post muestra como implementar un segmento de música simple en blucle (loop) usando Metasounds en Unreal Engine. Este segmento musical tiene cuatro “capas” que se reproducen de manera interactiva y en sincronía, una sobre la otra. Finalmente, muestro cómo llamar los diferentes cambios o estados en este metasound usando un reloj Quartz.
Implementé el mismo sistema dos veces para este proyecto de prueba, uno usando scripting visual Blueprints, y otro usando código nativo C++. Aquí la lista de herramientas y términos esenciales usados en este proyecto:
Programé todas las funcionalidades dentro de la clase C++ MusicPlayerActor, la clase blueprint BP_MusicPlayerActor, y el blueprint del nivel ThirdPersonMap.
Importante:
Ten en consideración que esta guía no pretende enseñar diseño de sonido creativo. En cambio, se enfoca exclusivamente en aspectos de implementación y programación de game audio.
DESCARGA
〰️
DESCARGA 〰️
Descarga el Proyecto Aquí:
DESCARGA
〰️
DESCARGA 〰️
Parte 2 - Quartz:
Dependencias:
Para usar el Subsistema Quartz y declarar propiedades Metasound, añade los módulos “AudioMixer” y “MetasoundEngine” al archivo Build.cs:
// Añade el módulo "MetasoundEngine" para usar Metasound Source. // Añade el módulo "AudioMixer" para usar el Subsistema Quartz. PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "MetasoundEngine", "AudioMixer"});
Declaración de directivas y propiedades en MusicPlayerActor:
Usa estas directivas en en archivo MusicPlayerActor.h para usar MetasoundSource y el Quartz Clock Handle. Por favor lee los comentarios en el código para más detalles:
#include "Quartz/AudioMixerClockHandle.h" //Incluye esta directiva para acceder al reloj Quartz. #include "MetasoundSource.h" //Incluye esta directiva para usar propiedades MetasoundSource.
He declarado estas propiedades en el archivo MusicPlayerActor.h. Por favor lee los comentarios en el código para más detalles:
/** Activa los mensajes en pantalla para debug. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Debug") bool bDebug = false; /** Segmento de música Metasound. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Music") UMetaSoundSource* MusicCue; /** El Audio component queue va a contender y controlar el segmento de música Metasound. */ UPROPERTY(EditDefaultsOnly, BlueprintReadWrite, Category="Music") UAudioComponent* MusicAudioComponent; /** El nombre del parámetro de tipo trigger. Cambia y actualiza el "estado" de la música en el Metasound. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Music") FName TriggerParameter; /** Reloj Quartz. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock") UQuartzClockHandle* MusicClock; /** Nombre del reloj Quartz. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock") FName NewClockName = "MusicClock"; /** El numerador de la métrica musical. Valor por defecto = 4. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock") int MeterNumerator = 4; /** El denominador de la métrica musical. Valor por defecto = QuarterNote. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock") EQuartzTimeSignatureQuantization MeterDenominator = EQuartzTimeSignatureQuantization::QuarterNote; /** Beats por minuto para el reloj Quartz. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock") float BPM = 76.0f; /** Número de barras o compáses para el loop en el segmento musical. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock") int BarNumbers = 8; /** Define el tiempo musical cuando las funciones se van a disparar. * Por defecto, esta enumeración dispara las funciones al final de una "redonda" o half note = 1/2. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Quartz Clock") EQuartzCommandQuantization QuantizationBoundary = EQuartzCommandQuantization::HalfNote;
Declaración de funciones y delegados en MusicPlayerActor:
Declaré estas funciones y delegados en el archivo MusicPlayerActor.h. Por favor lee los comentarios en el código para más detalles:
/** * @brief Crea y configura un nuevo reloj Quartz, y reproduce el segmento musical sincronizado. * @param ClockName Nombre para el nuevo reloj Quartz. * @param NumBeats Numerador de la métrica. * @param BeatType Denominador de la métrica. * @param BeatsPerMinute Beats por minuto / tempo. * @param AudioComponent Audio Component de la música. */ UFUNCTION(BlueprintCallable, Category="Music") void CreateClockAndPlayMusic(FName ClockName, int NumBeats, EQuartzTimeSignatureQuantization BeatType, float BeatsPerMinute, UAudioComponent* AudioComponent); /** * @brief Función delegada. Dispara su funcionalidad en sincronía con el reloj. * @param EventType Usa un switch en esta enumeración para seleccionar "CommandOnQueued". * @param Name */ UFUNCTION() void FPlayQuantizedDelegate(EQuartzCommandDelegateSubType EventType, FName Name); /** Se usa para enganchar FPlayQuantizedDelegate en el constructor de la clase. */ FOnQuartzCommandEventBP PlayQuantizationDelegate; /** * @brief Ejecuta un parámetro de tipo trigger sincronizado con el reloj. * @param Clock Reloj Quartz. * @param InQuantizationBoundary El tipo o límite de Cuantización. * @param ParameterName El parámetro a ser disparado en el segmento musical. */ UFUNCTION(BlueprintCallable, Category="Music") void ExecuteTriggerInTime(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, FName ParameterName); /** * @brief Función delegada. Dispara su funcionalidad en sincronía con el reloj. * @param ClockName Nombre del reloj Quartz. * @param QuantizationType * @param NumBars * @param Beat * @param BeatFraction */ UFUNCTION() void FExecuteTriggerDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction); /** Se usa para enganchar FPlayQuantizedDelegate en el constructor de la clase.*/ FOnQuartzMetronomeEventBP ExecuteTriggerDelegate; /** * @brief Reinicia el reloj en el último compás del loop musical. * @param Clock Reloj Quartz. * @param InQuantizationBoundary El tipo o límite de Cuantización. * @param NumBars Número de barras o compáses para el loop de segmento musical. */ UFUNCTION(BlueprintCallable, Category="Music") void UpdateAndResetClock(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, int NumBars); /** * @brief Función delegada. Dispara su funcionalidad en sincronía con el reloj. * @param ClockName Nombre del reloj Quartz. * @param QuantizationType * @param NumBars * @param Beat * @param BeatFraction */ UFUNCTION() void FUpdateClockDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction); /** Se usa para enganchar FPlayQuantizedDelegate en el constructor de la clase. */ FOnQuartzMetronomeEventBP UpdateClockDelegate;
Implementación C++ y Blueprint:
Creé una clase blueprint derivada de MusicPlayerActor, llamada BP_MusicPlayerActor. En esta sección, muestro las definiciones de las funciones de esta clase en C++ y el el Graph Blueprint.
El Constructor:
Todas las funciones en esta clase requieren de funciones delegadas que son disparadas en tiempo musical. Las funciones delegadas están enganchadas a UFunctions dentro del constructor de la clase. Adicionalmente creé y registré un Audio Component que va a contener el segmento musical.
MusicPlayerActor.cpp:
#include "Components/AudioComponent.h" //Incluye esta directiva para acceder a AudioComponents. // Define los valores por defecto. AMusicPlayerActor::AMusicPlayerActor() { /** Crea un component de audio. */ MusicAudioComponent = CreateDefaultSubobject<UAudioComponent>(TEXT("Audio Component")); MusicAudioComponent->SetAutoActivate(false); MusicAudioComponent->SetSound(MusicCue); /** Engancha todas las funciones delegadas. */ 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:
Crea y configura un nuevo reloj Quartz, y reproduce el segmento musical sincronizado.
MusicPlayerActor.cpp:
void AMusicPlayerActor::CreateClockAndPlayMusic(FName ClockName, int NumBeats, EQuartzTimeSignatureQuantization BeatType, float BeatsPerMinute, UAudioComponent* AudioComponent) { /** Obtiene una referencia del subsistema Quartz desde el mundo. */ UQuartzSubsystem* Quartz = GetWorld()->GetSubsystem<UQuartzSubsystem>(); /** Define las configuraciones para las structuras FQuartzTimeSignature y FQuartzClockSettings. */ FQuartzTimeSignature TimeSignature; TimeSignature.NumBeats = NumBeats; TimeSignature.BeatType = BeatType; FQuartzClockSettings ClockSettings; ClockSettings.TimeSignature = TimeSignature; /** Crea un reloj nuevo con las configuraciones de la estructura anterior.*/ MusicClock = Quartz->CreateNewClock(this, ClockName, ClockSettings, true); /** Sets the tempo for the clock. */ MusicClock->SetBeatsPerMinute(this, FQuartzQuantizationBoundary(), FOnQuartzCommandEventBP(), MusicClock, BeatsPerMinute); /** Define las configuraciones para la structura FQuartzQuantizationBoundary. */ FQuartzQuantizationBoundary QuartzQuantizationBoundary; QuartzQuantizationBoundary.Quantization = EQuartzCommandQuantization::Bar; QuartzQuantizationBoundary.Multiplier = 1.0f; QuartzQuantizationBoundary.CountingReferencePoint = EQuarztQuantizationReference::BarRelative; QuartzQuantizationBoundary.bResetClockOnQueued = true; /** Reproduce el segmento musical desde el component de audio después de queue el audio sea cargado y preparado. Empieza el reloj. */ AudioComponent->PlayQuantized(this, MusicClock, QuartzQuantizationBoundary, PlayQuantizationDelegate); } void AMusicPlayerActor::FPlayQuantizedDelegate(EQuartzCommandDelegateSubType EventType, FName Name) { /** Divide esta enumeración en diferentes caminos de ejecución. */ switch (EventType) { /** Empieza el reloj después de que la secuencia de audio esté lista y "en cola". */ case EQuartzCommandDelegateSubType::CommandOnQueued: MusicClock->StartClock(this, MusicClock); break; default: break; } if(bDebug) /** Si bDebug es verdad, añade estos mensajes a la pantalla. */ { GEngine->AddOnScreenDebugMessage(3, 5.0f, FColor::Green, "Music Start"); GEngine->AddOnScreenDebugMessage(4, 5.0f, FColor::Red, "C++ Implementation"); } }
BP_MusicPlayerActor:
Execute Trigger In Time:
Ejecuta un parámetro de tipo trigger sincronizado con el reloj.
MusicPlayerActor.cpp:
void AMusicPlayerActor::ExecuteTriggerInTime(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, FName ParameterName) { /** Obtiene una copia del nuevo parámetro trigger. */ TriggerParameter = ParameterName; /** Crea un delegado que va a dispararse sincronizdado con el reloj. */ Clock->SubscribeToQuantizationEvent(this, InQuantizationBoundary, ExecuteTriggerDelegate, Clock); } void AMusicPlayerActor::FExecuteTriggerDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction) { /** Define el nuevo parámetro a tiempo. */ MusicAudioComponent->SetTriggerParameter(TriggerParameter); /** De-registra a este delegado para que el parámetro trigger see dispare una sola vez.*/ MusicClock->UnsubscribeFromTimeDivision(this, QuantizationBoundary, MusicClock); if (bDebug) /** Si bDebug es verdad, añade estos mensajes a la pantalla. */ { GEngine->AddOnScreenDebugMessage(3, 5.0f, FColor::Magenta, TriggerParameter.ToString()); GEngine->AddOnScreenDebugMessage(4, 5.0f, FColor::Red, "C++ Implementation"); } }
BP_MusicPlayerActor:
Update And Reset Clock:
Reinicia el reloj en el último compás del loop musical.
MusicPlayerActor.cpp:
void AMusicPlayerActor::UpdateAndResetClock(UQuartzClockHandle* Clock, EQuartzCommandQuantization InQuantizationBoundary, int NumBars) { /** Crea un delegado que see dispara sincronizado con el reloj. */ Clock->SubscribeToQuantizationEvent(this, EQuartzCommandQuantization::EighthNote, UpdateClockDelegate, Clock); } void AMusicPlayerActor::FUpdateClockDelegate(FName ClockName, EQuartzCommandQuantization QuantizationType, int32 NumBars, int32 Beat, float BeatFraction) { /** Define las configuraciones para la structura FQuartzQuantizationBoundary. */ FQuartzQuantizationBoundary QuartzQuantizationBoundary; QuartzQuantizationBoundary.Quantization = QuantizationBoundary; QuartzQuantizationBoundary.Multiplier = 1.0f; QuartzQuantizationBoundary.CountingReferencePoint = EQuarztQuantizationReference::BarRelative; /** Si la posición actual del compás es igual al número total de compáses de la música, reinicia el reloj al final del límite de la barra.*/ if(NumBars == BarNumbers) MusicClock->ResetTransportQuantized(this, QuantizationBoundary, FOnQuartzCommandEventBP(), MusicClock); /** Si bDebug es verdad, añade estos mensajes a la pantalla. */ 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:
Implementación CPP completa:
El Blueprint del Nivel:
Para este proyecto he creado cuatro trigger volumes que llaman a las funciones desde BP_MusicPlayerActor cuando el Player Character se sobrepone con ellos. Aquí muestro cómo implementé el sistema que reproduce y modifica la mezcla de la música. Para abrir el blueprint del nivel, da click en el ícono encima del viewport: blueprint class.
El Viewport:
Implementación del blueprint del nivel:
Eventos:
Da click en las imágenes para abrirlas.