Wwise Implementation Demo | Unreal Implementation Part 2

C++ Functions

Big thanks to Jamie Carnell for putting up with my many questions about C++ and the Wwise API throughout this learning process.

Audio Statics Library

Get Surface Type By Line Trace

To begin the process of converting my blueprints into C++ classes, I created a Blueprint Function Library to house all audio related statics I might need to call in various aspects of the game. These functions were given “UFUNCTION” properties and set to be “BlueprintCallable” so that I could implement them within the various Actors and Components I have created previously.

The first of these functions I created was a method to return the Surface Type of any actor set to block the specified trace channel argument. This works by calling a line trace from the Location argument, along the world down vector until either the “TraceDistance” specified, or a collision occurs. This hit result is then queried to obtain the Surface Type of the hit actor and return it to where-ever this function is called.

Debug options are provided, such as line and spheres to visualize where the line trace is being cast, and where the hit is being registered. Several checks are run during this method to ensure null pointers are not encountered, and that the actor calling this function does not trace to itself.

Post Ak Event

The Wwise API provides many ways to post an audio event, with AKGameplayStatics being the highest level and simplest function set and Ak sound engine providing a more granular level of control. For this project, I decided to create my own custom functions that allowed access to the Ak component created to post the audio event.

Returning the Ak Component reference allows me to call actions on the component (see: AkExtensions) as well as update parameters at runtime.

There are two variations of the post event function, “At location” and “Attached”. As the names suggest, “At location” will create a self-destructing temporary Ak component that can be position in world space, and “attached” requires a component reference to be attached to (follows world position).

There are more functions that I have not covered in this breakdown included in this function library. The full cpp class can be found below:

BPFL_AudioStatics.cpp

#include "BPFL_AudioStatics.h"

#include "AkAudioEvent.h"
#include "AkComponent.h"
#include "AkExtensions.h"
#include "AkGameplayStatics.h"
#include "S_AudioStructs.h"
#include "Kismet/GameplayStatics.h"

/*
Static EPhysicalSurface function. Linecasts via trace channel negatively along the world up vector.
Returns an EPhysicalSurface type, conditionally determined by the Physical Material on the hit actor,
or returning a defined default return value in the cast of NULL.
*/
#pragma region GetSurfaceTypeByLineTrace
EPhysicalSurface UBPFL_AudioStatics::GetSurfaceTypeByLineTrace
(
	AActor* Actor,
	float TraceDistance,
	FVector Location,
	TEnumAsByte<ECollisionChannel> TraceChannel,
	bool DebugEnabled)
{
	if (!IsValid(Actor))
	{
		if (DebugEnabled)
		{
			UE_LOG(LogTemp, Log, TEXT("Actor Not Valid"));
		}
		return EPhysicalSurface::SurfaceType_Default;
	}

	FVector TraceStart = Location;
	FVector TraceEnd = Location + FVector::DownVector * TraceDistance;

	FCollisionQueryParams QueryParams;
	QueryParams.AddIgnoredActor(Actor);
	QueryParams.bTraceComplex = false;
	QueryParams.bReturnPhysicalMaterial = true;

	FHitResult Hit;
	Actor->GetWorld()->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, TraceChannel, QueryParams);

	if (DebugEnabled)
	{
		DrawDebugLine(Actor->GetWorld(), TraceStart, Hit.Location, Hit.bBlockingHit ? FColor::Blue : FColor::Red, false, 5.0f, 0, 10.0f);
	}

	EPhysicalSurface Surface = EPhysicalSurface::SurfaceType_Default;
	if (Hit.bBlockingHit && Hit.PhysMaterial != nullptr)
	{
		Surface = UGameplayStatics::GetSurfaceType(Hit);
	}

	return Surface;
}
#pragma endregion

// This region contains functions related to posting Ak events
#pragma region Post Events
/*
Static Function to post an Ak event attached to  with parameters. 
Parameters in be specified in BP by creating struct array variables for FSwitchValue & FRTPCValue.
Instances of this component can have their volumes offset by a value 0-1, with 1 representing the level defined in Wwise.
*/
int32 UBPFL_AudioStatics::PostAkEventAttached
(
	USceneComponent* AttachToComponent,
	UAkAudioEvent* AkEvent,
	const float VolumeOffset,
	const TArray<FSwitchValue> SwitchValues,
	const TArray<FRTPCValue> RTPCValues,
	EAttachLocation::Type LocationType,
	UAkComponent*& AkComponentOut,
	bool& ComponentCreated
)
{
	// Get or initialize the AkComponent attached to the scene component
	UAkComponent* AkComponent = UAkGameplayStatics::GetAkComponent(AttachToComponent, ComponentCreated, NAME_None, ((FVector)(ForceInit)), LocationType);

	// Check if AkComponent is nullptr, return AK_INVALID_PLAYING_ID if it is
	if (UNLIKELY(!IsValid(AkComponent)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Invalid AkComponent"));
		return AK_INVALID_PLAYING_ID;
	}

	AkComponentOut = AkComponent;

	// Check if AkEvent is nullptr, return AK_INVALID_PLAYING_ID if it is
	if (UNLIKELY(!IsValid(AkEvent)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Invalid AkAudioEvent"));
		return AK_INVALID_PLAYING_ID;
	}

	// Update all Switch values before posting the event
	UpdateSwitches(AkComponent, SwitchValues);

	// Update all RTPC values before posting the event
	UpdateRTPCs(AkComponent, RTPCValues);

	// Set volume offset for Ak Event
	AkComponent->SetOutputBusVolume(VolumeOffset);

	// Post Ak Event
	return AkComponent->PostAkEvent(AkEvent);
}

/*
Static Function to post an Ak event to a temporary Ak component at a location with parameters.
Parameters in be specified in BP by creating struct array variables for FSwitchValue & FRTPCValue.
Instances of this component can have their volumes offset by a value 0-1, with 1 representing the level defined in Wwise.
*/
int32 UBPFL_AudioStatics::PostAkEventAtLocation
(
	UObject* WorldContextObject,
	UAkAudioEvent* AkEvent,
	const FVector Location,
	const FRotator Rotator,
	const float VolumeOffset,
	const bool AutoDestroy,
	const TArray<FSwitchValue> SwitchValues,
	const TArray<FRTPCValue> RTPCValues,
	EAttachLocation::Type LocationType,
	UAkComponent*& AkComponentOut,
	bool& ComponentCreated
)
{
	// Get or initialize the AkComponent attached to the scene component
	UAkComponent* AkComponent = UAkGameplayStatics::SpawnAkComponentAtLocation(WorldContextObject, AkEvent, Location, Rotator, false, TEXT(""), AutoDestroy);

	// Check if AkComponent is nullptr, return AK_INVALID_PLAYING_ID if it is
	if (UNLIKELY(!IsValid(AkComponent)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Invalid AkComponent"));
		return AK_INVALID_PLAYING_ID;
	}

	AkComponentOut = AkComponent;

	// Check if AkEvent is nullptr, return AK_INVALID_PLAYING_ID if it is
	if (UNLIKELY(!IsValid(AkEvent)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Invalid AkAudioEvent"));
		return AK_INVALID_PLAYING_ID;
	}

	// Update all Switch values before posting the event
	UpdateSwitches(AkComponent, SwitchValues);

	// Update all RTPC values before posting the event
	UpdateRTPCs(AkComponent, RTPCValues);

	// Set volume offset for Ak Event
	AkComponent->SetOutputBusVolume(VolumeOffset);

	// Post Ak Event
	return AkComponent->PostAkEvent(AkEvent);
}
#pragma endregion

// This region contains functions related to updating parameters
#pragma region Update Parameters
// Function to update RTPC values for an AkComponent
void UBPFL_AudioStatics::UpdateRTPCs(UAkComponent* AkComponent, const TArray<FRTPCValue> RTPCValues)
{
	// Iterate through all RTPC values and set their relative values accordingly
	for (const auto& RTPCValue : RTPCValues)
	{
		AkComponent->SetRTPCValue(RTPCValue.RTPC, RTPCValue.Value, RTPCValue.InterpolationTimeMs, RTPCValue.RTPCString);
	}
}

// Function to update switch values for an AkComponent
void UBPFL_AudioStatics::UpdateSwitches(UAkComponent* AkComponent, const TArray<FSwitchValue> SwitchValues)
{
	// Iterate through all switch values and set their values accordingly
	for (const auto& SwitchValue : SwitchValues)
	{
		AkComponent->SetSwitch(SwitchValue.Switch, SwitchValue.SwitchGroup, SwitchValue.SwitchState);
	}
}
#pragma endregion

#pragma region Tools
const TArray<FSwitchValue> UBPFL_AudioStatics::MakeSwitchArray(UAkSwitchValue* SwitchValue, FString SwitchGroup, FString SwitchState)
{
	TArray<FSwitchValue> SwitchValues;
	SwitchValues.Add({ SwitchValue, SwitchGroup, SwitchState });
	return SwitchValues;
}

#pragma endregion

AkExtensions

I decided to create a seperate function library class to contain modified versions of exisitng methods found in the Wwise API. One of these modified functions that was essential to this project was “ExecuteActionOnAkGameObject”. This function allow actions such as “stop” and “pause” to be called on Ak audio events using an Ak component reference.

The original version of this function only allowed for actions to be called on UActors, but then the function itself obtained the Ak Component reference from the UActor reference. It was a relatively straightforward process to make this adaptation, and honestly I’m not sure why it is not an existing feature.

BPFL_AkExtensions.cpp

#include "AkExtensions.h"

#include "AkAudioDevice.h"
#include "AkAudioEvent.h"
#include "AkGameObject.h"
#include "Wwise/API/WwiseSoundEngineAPI.h"

int32 UAkExtensions::ExecuteActionOnAkGameObject(const AkActionOnEventType ActionType, const UAkGameObject* AkGameObject, const UAkAudioEvent* AkEvent, const int32 PlayingID, const int32 TransitionDuration, const EAkCurveInterpolation FadeCurve)
{
	const auto* AudioDevice = FAkAudioDevice::Get();
	if (UNLIKELY(!AudioDevice))
	{
		//UE_LOG(LogAkAudio, Verbose, TEXT("Failed to execute an action on AkAudioEvent '%s' without an Audio Device."), *GetName());
		return AK_NotInitialized;
	}

	if (UNLIKELY(!AudioDevice->IsInitialized()))
	{
		//UE_LOG(LogAkAudio, Verbose, TEXT("Failed to execute an action on AkAudioEvent '%s' with the Sound Engine uninitialized."), *GetName());
		return AK_NotInitialized;
	}

	auto* SoundEngine = IWwiseSoundEngineAPI::Get();
	if (UNLIKELY(!SoundEngine))
	{
		//UE_LOG(LogAkAudio, Warning, TEXT("Failed to execute an action on AkAudioEvent '%s' without a Sound Engine."), *GetName());
		return AK_NotInitialized;
	}

	if (!IsValid(AkGameObject))
	{
		return SoundEngine->ExecuteActionOnEvent(AkEvent->GetShortID(),
			static_cast<AK::SoundEngine::AkActionOnEventType>(ActionType),
			AK_INVALID_GAME_OBJECT,
			TransitionDuration,
			static_cast<AkCurveInterpolation>(FadeCurve),
			PlayingID
		);
	}

	if (UNLIKELY(IsValid(AkGameObject->GetOwner()) && AkGameObject->GetOwner()->IsActorBeingDestroyed()))
	{
		//UE_LOG(LogAkAudio, Error, TEXT("Failed to execute on AkAudioEvent '%s' with an actor that's not valid."), *GetName());
		return AK_InvalidParameter;
	}

	return SoundEngine->ExecuteActionOnEvent(AkEvent->GetShortID(),
		static_cast<AK::SoundEngine::AkActionOnEventType>(ActionType),
		AkGameObject->GetAkGameObjectID(),
		TransitionDuration,
		static_cast<AkCurveInterpolation>(FadeCurve),
		PlayingID
	);
}

References

Sources:

AudioKinetic, 2022. Spatial Audio. [Online]
Available at: https://www.audiokinetic.com/en/products/wwise-spatial-audio/
[Accessed 04 12 2022].
Jacobsen, B., 2023. Cujo Sound Design. [Online]
Available at: https://cujo.dk
[Accessed 05 01 2023].
Looman, T., 2023. Unreal Engine C++ Complete Guide. [Online]
Available at: https://www.tomlooman.com/unreal-engine-cpp-guide/
[Accessed 01 05 2023].
Shores, I., 2021. Portals:The Secret Weapon of AAA Game Audio Spatialization. Redmond, WA, Digipen Institute of Technology.

Assets:

Ancient Greek Music – Second Delphic Hymn to Apollo (Paean and Processional)

Greek Island – Scale3D