Files
UnrealEngine/Engine/Source/Runtime/AudioAnalyzer/Private/AudioAnalyzerNRT.cpp
2025-05-18 13:04:45 +08:00

325 lines
8.9 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AudioAnalyzerNRT.h"
#include "AudioAnalyzerNRTFacade.h"
#include "AudioAnalyzerModule.h"
#include "SampleBuffer.h"
#include "Async/Async.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(AudioAnalyzerNRT)
#if WITH_EDITOR
namespace
{
class FAudioAnalyzeNRTTask : public FNonAbandonableTask
{
friend class FAutoDeleteAsyncTask<FAudioAnalyzeNRTTask>;
public:
FAudioAnalyzeNRTTask(
TWeakObjectPtr<UAudioAnalyzerNRT> InAnalyzerUObject,
const UAudioAnalyzerNRT::FResultId InResultId,
TUniquePtr<Audio::FAnalyzerNRTFacade>&& InAnalyzerFacade,
TArray<uint8>&& InRawWaveData,
int32 InNumChannels,
float InSampleRate)
: AnalyzerUObject(InAnalyzerUObject)
, ResultId(InResultId)
, AnalyzerFacade(MoveTemp(InAnalyzerFacade))
, RawWaveData(MoveTemp(InRawWaveData))
, NumChannels(InNumChannels)
, SampleRate(InSampleRate)
{}
void DoWork()
{
TUniquePtr<Audio::IAnalyzerNRTResult> Result = AnalyzerFacade->AnalyzePCM16Audio(RawWaveData, NumChannels, SampleRate);
UAudioAnalyzerNRT::FResultSharedPtr ResultPtr(Result.Release());
// Set value on game thread.
AsyncTask(ENamedThreads::GameThread, [Analyzer = AnalyzerUObject, ThisResultId = ResultId, ResultPtr]() {
if (Analyzer.IsValid())
{
Analyzer->SetResultIfLatest(ResultPtr, ThisResultId);
}
});
}
FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(AudioAnalyzeNRTTask, STATGROUP_ThreadPoolAsyncTasks); }
private:
TWeakObjectPtr<UAudioAnalyzerNRT> AnalyzerUObject;
const UAudioAnalyzerNRT::FResultId ResultId;
TUniquePtr<Audio::FAnalyzerNRTFacade> AnalyzerFacade;
TArray<uint8> RawWaveData;
int32 NumChannels;
float SampleRate;
};
}
/*****************************************************/
/*********** UAudioAnalyzerNRTSettings ***************/
/*****************************************************/
void UAudioAnalyzerNRTSettings::PostEditChangeProperty (struct FPropertyChangedEvent & PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
if (ShouldEventTriggerAnalysis(PropertyChangedEvent))
{
AnalyzeAudioDelegate.Broadcast();
}
}
bool UAudioAnalyzerNRTSettings::ShouldEventTriggerAnalysis(struct FPropertyChangedEvent & PropertyChangeEvent)
{
// By default, all non-interactive changes to settings will trigger analysis.
return PropertyChangeEvent.ChangeType != EPropertyChangeType::Interactive;
}
/*****************************************************/
/*********** UAudioAnalyzerNRT ***************/
/*****************************************************/
void UAudioAnalyzerNRT::PreEditChange(FProperty* PropertyAboutToChange)
{
// If the settings object is replaced, need to unbind any existing settings objects
// from calling the analyze audio delegate.
Super::PreEditChange(PropertyAboutToChange);
UAudioAnalyzerNRTSettings* Settings = GetSettingsFromProperty(PropertyAboutToChange);
if (Settings)
{
RemoveSettingsDelegate(Settings);
}
}
void UAudioAnalyzerNRT::PostEditChangeProperty (struct FPropertyChangedEvent & PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);
// Check if the edited property was a UAudioAnalyzerNRTSettings object
UAudioAnalyzerNRTSettings* Settings = GetSettingsFromProperty(PropertyChangedEvent.Property);
if (Settings)
{
// If it was a UAudioAnalyzerNRTSettings object, bind the FAnalyzeAudioDelegate
SetSettingsDelegate(Settings);
}
if (ShouldEventTriggerAnalysis(PropertyChangedEvent))
{
AnalyzeAudio();
}
}
bool UAudioAnalyzerNRT::ShouldEventTriggerAnalysis(struct FPropertyChangedEvent & PropertyChangeEvent)
{
// by default, all changes will trigger analysis
return true;
}
void UAudioAnalyzerNRT::AnalyzeAudio()
{
AUDIO_ANALYSIS_LLM_SCOPE
// Create a new result id for this result.
FResultId ThisResultId = ++CurrentResultId;
if (nullptr != Sound)
{
if (Sound->bProcedural)
{
UE_LOG(LogAudioAnalyzer, Warning, TEXT("Soundwave '%s' is procedural. NRT audio analysis is not currently supported for this."), *Sound->GetFullName());
SetResult(nullptr);
return;
}
// Read audio while Sound object is assured safe.
if (Sound->ChannelSizes.Num() > 0)
{
UE_LOG(LogAudioAnalyzer, Warning, TEXT("Soundwave '%s' has multi-channel audio (channels greater than 2). Audio analysis is not currently supported for this yet."), *Sound->GetFullName());
SetResult(nullptr);
return;
}
// Retrieve the raw imported data
TArray<uint8> RawWaveData;
uint32 SampleRate = 0;
uint16 NumChannels = 0;
if (!Sound->GetImportedSoundWaveData(RawWaveData, SampleRate, NumChannels))
{
UE_LOG(LogAudioAnalyzer, Error, TEXT("Could not analyze audio due to failed import of sound wave data from Soundwave '%s'."), *Sound->GetFullName());
SetResult(nullptr);
return;
}
if (SampleRate == 0 || NumChannels == 0)
{
UE_LOG(LogAudioAnalyzer, Error, TEXT("Failed to parse the raw imported data for '%s' for analysis."), *Sound->GetFullName());
SetResult(nullptr);
return;
}
// Create analyzer helper object
TUniquePtr<Audio::FAnalyzerNRTFacade> BatchAnalyzer = MakeUnique<Audio::FAnalyzerNRTFacade>(GetSettings(SampleRate, NumChannels), GetAnalyzerNRTFactoryName());
// Use weak reference in case this object is deleted before analysis is done
TWeakObjectPtr<UAudioAnalyzerNRT> AnalyzerPtr(this);
// Create and start async task. Parentheses avoids memory leak warnings from static analysis.
(new FAutoDeleteAsyncTask<FAudioAnalyzeNRTTask>(AnalyzerPtr, ThisResultId, MoveTemp(BatchAnalyzer), MoveTemp(RawWaveData), NumChannels, SampleRate))->StartBackgroundTask();
}
else
{
// Copy empty result to this object
SetResult(nullptr);
}
}
// Returns UAudioAnalyzerNRTSettings* if property points to a valid UAudioAnalyzerNRTSettings, otherwise returns nullptr.
UAudioAnalyzerNRTSettings* UAudioAnalyzerNRT::GetSettingsFromProperty(FProperty* Property)
{
if (nullptr == Property)
{
return nullptr;
}
if (Property->IsA(FObjectPropertyBase::StaticClass()))
{
FObjectPropertyBase* ObjectPropertyBase = CastFieldChecked<FObjectPropertyBase>(Property);
if (nullptr == ObjectPropertyBase)
{
return nullptr;
}
if (ObjectPropertyBase->PropertyClass->IsChildOf(UAudioAnalyzerNRTSettings::StaticClass()))
{
UObject* PropertyObject = ObjectPropertyBase->GetObjectPropertyValue_InContainer(this);
return Cast<UAudioAnalyzerNRTSettings>(PropertyObject);
}
}
return nullptr;
}
void UAudioAnalyzerNRT::SetResult(FResultSharedPtr NewResult)
{
FScopeLock ResultLock(&ResultCriticalSection);
Result = NewResult;
if (Result.IsValid())
{
DurationInSeconds = Result->GetDurationInSeconds();
}
else
{
DurationInSeconds = 0.f;
}
Modify();
}
void UAudioAnalyzerNRT::SetResultIfLatest(FResultSharedPtr NewResult, FResultId InResultId)
{
FScopeLock ResultLock(&ResultCriticalSection);
const FResultId ResultId = CurrentResultId.Load();
if (ResultId == InResultId)
{
Result = NewResult;
if (Result.IsValid())
{
DurationInSeconds = Result->GetDurationInSeconds();
}
else
{
DurationInSeconds = 0.f;
}
Modify();
}
}
#endif
void UAudioAnalyzerNRT::Serialize(FArchive& Ar)
{
// default uobject serialize
Super::Serialize(Ar);
// When loading object, Result pointer is invalid. Need to create a valid
// result object for loading.
if (!Result.IsValid())
{
if (!GetClass()->HasAnyClassFlags(CLASS_Abstract))
{
Audio::IAnalyzerNRTFactory* Factory = Audio::GetAnalyzerNRTFactory(GetAnalyzerNRTFactoryName());
if (nullptr != Factory)
{
// Create result and worker from factory
{
FScopeLock ResultLock(&ResultCriticalSection);
Result = Factory->NewResultShared<ESPMode::ThreadSafe>();
}
}
}
}
if (Result.IsValid())
{
FScopeLock ResultLock(&ResultCriticalSection);
Result->Serialize(Ar);
}
}
TUniquePtr<Audio::IAnalyzerNRTSettings> UAudioAnalyzerNRT::GetSettings(const float InSampleRate, const int32 InNumChannels) const
{
AUDIO_ANALYSIS_LLM_SCOPE
return MakeUnique<Audio::IAnalyzerNRTSettings>();
}
#if WITH_EDITOR
void UAudioAnalyzerNRT::SetSettingsDelegate(UAudioAnalyzerNRTSettings* InSettings)
{
if (InSettings)
{
if (AnalyzeAudioDelegateHandles.Contains(InSettings))
{
// Avoid setting delegate more tha once
return;
}
FDelegateHandle DelegateHandle = InSettings->AnalyzeAudioDelegate.AddUObject(this, &UAudioAnalyzerNRT::AnalyzeAudio);
if (DelegateHandle.IsValid())
{
AnalyzeAudioDelegateHandles.Add(InSettings, DelegateHandle);
}
}
}
void UAudioAnalyzerNRT::RemoveSettingsDelegate(UAudioAnalyzerNRTSettings* InSettings)
{
if (InSettings)
{
if (AnalyzeAudioDelegateHandles.Contains(InSettings))
{
FDelegateHandle DelegateHandle = AnalyzeAudioDelegateHandles[InSettings];
if (DelegateHandle.IsValid())
{
InSettings->AnalyzeAudioDelegate.Remove(DelegateHandle);
}
AnalyzeAudioDelegateHandles.Remove(InSettings);
}
}
}
#endif