// Copyright Epic Games, Inc. All Rights Reserved. #include "SoundModulationDestinationLayout.h" #include "Algo/AnyOf.h" #include "AudioDevice.h" #include "AudioDeviceManager.h" #include "AudioEditorModule.h" #include "Containers/Map.h" #include "Containers/UnrealString.h" #include "Delegates/Delegate.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Editor.h" #include "Editor/EditorEngine.h" #include "Engine/Engine.h" #include "Engine/World.h" #include "Fonts/SlateFontInfo.h" #include "Framework/SlateDelegates.h" #include "HAL/PlatformCrt.h" #include "IAudioExtensionPlugin.h" #include "IAudioModulation.h" #include "IDetailChildrenBuilder.h" #include "IDetailPropertyRow.h" #include "Input/Reply.h" #include "Internationalization/Internationalization.h" #include "Internationalization/Text.h" #include "Layout/Margin.h" #include "Layout/Visibility.h" #include "Logging/LogCategory.h" #include "Logging/LogMacros.h" #include "Math/UnrealMathSSE.h" #include "Misc/AssertionMacros.h" #include "Misc/Attribute.h" #include "Misc/CString.h" #include "PropertyEditorModule.h" #include "PropertyHandle.h" #include "SlotBase.h" #include "Sound/SoundModulationDestination.h" #include "Sound/SoundSourceBus.h" #include "Styling/AppStyle.h" #include "Templates/Casts.h" #include "Trace/Detail/Channel.h" #include "Types/SlateEnums.h" #include "UObject/NameTypes.h" #include "UObject/Object.h" #include "UObject/UnrealType.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SButton.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Text/STextBlock.h" #define LOCTEXT_NAMESPACE "SoundModulationParameter" namespace ModDestinationLayoutUtils { IAudioModulationManager* GetEditorModulationManager() { if (GEditor) { if (UWorld* World = GEditor->GetEditorWorldContext().World()) { FAudioDeviceHandle AudioDeviceHandle = World->GetAudioDevice(); if (AudioDeviceHandle.IsValid() && AudioDeviceHandle->IsModulationPluginEnabled()) { return AudioDeviceHandle->ModulationInterface.Get(); } } } return nullptr; } bool IsModulationEnabled() { return GetEditorModulationManager() != nullptr; } FName GetParameterNameFromMetaData(const TSharedRef& InHandle) { static const FName AudioParamFieldName("AudioParam"); if (InHandle->HasMetaData(AudioParamFieldName)) { FString ParamString = InHandle->GetMetaData(AudioParamFieldName); return FName(ParamString); } return FName(); } FName GetParameterClassFromMetaData(const TSharedRef& InHandle) { static const FName AudioParamClassFieldName("AudioParamClass"); if (InHandle->HasMetaData(AudioParamClassFieldName)) { const FName Param = *(InHandle->GetMetaData(AudioParamClassFieldName)); return Param; } return FName(); } bool IsParamMismatched(TSharedRef ModulatorHandle, TSharedRef StructPropertyHandle, FName* OutModParamClassName = nullptr, FName* OutDestParamClassName = nullptr, FName* OutModName = nullptr) { if (OutModParamClassName) { *OutModParamClassName = FName(); } if (OutDestParamClassName) { *OutDestParamClassName = FName(); } if (OutModName) { *OutModName = FName(); } UObject* ModObject = nullptr; ModulatorHandle->GetValue(ModObject); USoundModulatorBase* ModBase = Cast(ModObject); if (!ModBase) { return false; } const FName ModParamClassName = ModBase->GetOutputParameter().ClassName; const FName DestParamClassName = ModDestinationLayoutUtils::GetParameterClassFromMetaData(StructPropertyHandle); if (!ModParamClassName.IsNone() && !DestParamClassName.IsNone() && ModParamClassName != DestParamClassName) { if (OutModParamClassName) { *OutModParamClassName = ModParamClassName; } if (OutDestParamClassName) { *OutDestParamClassName = DestParamClassName; } if (OutModName) { *OutModName = ModBase->GetFName(); } return true; } return false; } FText SetMetaData(TSharedRef StructPropertyHandle, TSharedRef ValueHandle, FText& OutUnitDisplayText, FName& OutParamName) { bool bClampValuesSet = false; float ClampMinValue = 0.0f; float ClampMaxValue = 1.0f; float UIMinValue = 0.0f; float UIMaxValue = 1.0f; if (StructPropertyHandle->HasMetaData("ClampMin")) { bClampValuesSet = true; FString ParamString = StructPropertyHandle->GetMetaData("ClampMin"); ClampMinValue = FCString::Atof(*ParamString); } if (StructPropertyHandle->HasMetaData("ClampMax")) { FString ParamString = StructPropertyHandle->GetMetaData("ClampMax"); ClampMaxValue = FCString::Atof(*ParamString); bClampValuesSet = true; } OutParamName = ModDestinationLayoutUtils::GetParameterNameFromMetaData(StructPropertyHandle); if (OutParamName != FName()) { // If parameter was provided, it overrides ClampMin/Max. if (const Audio::FModulationParameter* Parameter = Audio::GetModulationParameterPtr(OutParamName)) { // if no valid parameter was found & the user has specified their own clamping, don't override it with the default parameter range of [0,1] if (false == (Parameter->ParameterName == FName() && bClampValuesSet)) { UIMinValue = Parameter->MinValue; UIMaxValue = Parameter->MaxValue; ClampMinValue = UIMinValue; ClampMaxValue = UIMaxValue; OutUnitDisplayText = Parameter->UnitDisplayName; if (bClampValuesSet) { UE_LOG(LogAudioEditor, Verbose, TEXT("ClampMin/Max overridden by AudioModulation plugin asset with ParamName '%s'."), *OutParamName.ToString()); } } } } // User data overrides UIMin/Max if its in clamp range. if (StructPropertyHandle->HasMetaData("UIMin")) { float NewMin = UIMinValue; FString ParamString = StructPropertyHandle->GetMetaData("UIMin"); NewMin = FCString::Atof(*ParamString); UIMinValue = FMath::Clamp(NewMin, ClampMinValue, ClampMaxValue); } if (StructPropertyHandle->HasMetaData("UIMax")) { float NewMax = UIMaxValue; FString ParamString = StructPropertyHandle->GetMetaData("UIMax"); NewMax = FCString::Atof(*ParamString); UIMaxValue = FMath::Clamp(NewMax, ClampMinValue, ClampMaxValue); } ValueHandle->SetInstanceMetaData("ClampMin", FString::Printf(TEXT("%f"), ClampMinValue)); ValueHandle->SetInstanceMetaData("ClampMax", FString::Printf(TEXT("%f"), ClampMaxValue)); ValueHandle->SetInstanceMetaData("UIMin", FString::Printf(TEXT("%f"), UIMinValue)); ValueHandle->SetInstanceMetaData("UIMax", FString::Printf(TEXT("%f"), UIMaxValue)); return OutUnitDisplayText; } void CustomizeChildren_AddValueRow( IDetailChildrenBuilder& ChildBuilder, TSharedRef StructPropertyHandle, TSharedRef ValueHandle, TSharedRef ModulatorsHandle, TSharedRef EnablementHandle) { FText UnitDisplayText = FText::GetEmpty(); FName ParamName; SetMetaData(StructPropertyHandle, ValueHandle, UnitDisplayText, ParamName); const FText DisplayName = StructPropertyHandle->GetPropertyDisplayName(); ChildBuilder.AddCustomRow(DisplayName) .NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(DisplayName) .ToolTipText(StructPropertyHandle->GetToolTipText()) ] .ValueContent() .MinDesiredWidth(250.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(4.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) [ ValueHandle->CreatePropertyValueWidget() ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(UnitDisplayText) .ToolTipText(ValueHandle->GetToolTipText()) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) [ EnablementHandle->CreatePropertyValueWidget() ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) [ EnablementHandle->CreatePropertyNameWidget() ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) [ SNew(SButton) .ToolTipText(LOCTEXT("ResetToParameterDefaultToolTip", "Reset to parameter's default")) .ButtonStyle(FAppStyle::Get(), TEXT("NoBorder")) .ContentPadding(0.0f) .Visibility(TAttribute::Create([ParamName, ValueHandle] { float CurrentValue = 0.0f; ValueHandle->GetValue(CurrentValue); const Audio::FModulationParameter* Parameter = Audio::GetModulationParameterPtr(ParamName); if (Parameter && Parameter->DefaultValue == CurrentValue) { return EVisibility::Hidden; } return EVisibility::Visible; })) .OnClicked(FOnClicked::CreateLambda([ParamName, ValueHandle]() { if (const Audio::FModulationParameter* Parameter = Audio::GetModulationParameterPtr(ParamName)) { ValueHandle->SetValue(Parameter->DefaultValue); } return FReply::Handled(); })) .Content() [ SNew(SImage) .Image(FAppStyle::GetBrush("PropertyWindow.DiffersFromDefault")) ] ] ]; EnablementHandle->SetOnPropertyValueChanged(FSimpleDelegate::CreateLambda([EnablementHandle, ValueHandle, StructPropertyHandle, ModulatorsHandle]() { bool bEnabled = false; EnablementHandle->GetValue(bEnabled); if (bEnabled) { const FName ParamName = ModDestinationLayoutUtils::GetParameterNameFromMetaData(StructPropertyHandle); if (const Audio::FModulationParameter* Parameter = Audio::GetModulationParameterPtr(ParamName)) { ValueHandle->SetValue(Parameter->DefaultValue); return; } } ModulatorsHandle->AsSet()->Empty(); })); } void CustomizeChildren_AddValueNoModRow(IDetailChildrenBuilder& ChildBuilder, TSharedRef StructPropertyHandle, TSharedRef ValueHandle) { FText UnitDisplayText = FText::GetEmpty(); FName ParamName; SetMetaData(StructPropertyHandle, ValueHandle, UnitDisplayText, ParamName); const FText DisplayName = StructPropertyHandle->GetPropertyDisplayName(); FDetailWidgetRow& ValueNoModRow = ChildBuilder.AddCustomRow(DisplayName); ValueNoModRow.NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(DisplayName) .ToolTipText(StructPropertyHandle->GetToolTipText()) ] .ValueContent() .MinDesiredWidth(120.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(4.0f, 0.0f, 0.0f, 0.0f) .VAlign(VAlign_Center) [ ValueHandle->CreatePropertyValueWidget() ] ]; } void CustomizeChildren_AddModulatorRow(IDetailChildrenBuilder& ChildBuilder, TSharedRef StructPropertyHandle, TSharedRef ModulatorsHandle, TSharedRef EnablementHandle) { const FText DisplayName = StructPropertyHandle->GetPropertyDisplayName(); auto ModEnabled = [EnablementHandle]() { bool bModulationEnabled = false; EnablementHandle->GetValue(bModulationEnabled); return bModulationEnabled ? EVisibility::Visible : EVisibility::Collapsed; }; const TAttribute ModulatorVisibility = TAttribute::Create(ModEnabled); ChildBuilder.AddProperty(ModulatorsHandle) .DisplayName(FText::Format(LOCTEXT("SoundModulationParameter_ModulatorsFormat", "{0} Modulators"), DisplayName)) .Visibility(ModulatorVisibility); ChildBuilder.AddCustomRow(LOCTEXT("SoundModulationDestinationLayout_UnitMismatchHeadingWarning", "Unit Mismatch Warning")) .NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(FText::Format(LOCTEXT("ModulationDestinationLayout_UnitMismatchHeader", "{0} Mismatched Parameters"), DisplayName)) .ToolTipText(StructPropertyHandle->GetToolTipText()) ] .ValueContent() .MinDesiredWidth(150.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0f) .Padding(0.0f, 3.0f, 0.0f, 3.0f) .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFontBold()) .Text(TAttribute::Create([ModSet = ModulatorsHandle->AsSet(), StructPropertyHandle]() { uint32 NumElements = 0; ModSet->GetNumElements(NumElements); FString MismatchedText(""); for (int32 i = 0; i < (int32)NumElements; ++i) { TSharedRef SetMemberHandle = ModSet->GetElement(i); FName ModParamClassName; FName DestClassName; FName ModName; if (ModDestinationLayoutUtils::IsParamMismatched(SetMemberHandle, StructPropertyHandle, &ModParamClassName, &DestClassName, &ModName)) { MismatchedText += FText::Format( LOCTEXT("ModulationDestinationLayout_UnitMismatchesFormat", "{0} ({1}), Expected: {2}\n"), FText::FromName(ModName), FText::FromName(ModParamClassName), FText::FromName(DestClassName) ).ToString(); } } return FText::FromString(MismatchedText); })) ] ] .Visibility(TAttribute::Create([ModSet = ModulatorsHandle->AsSet(), ModEnabled, StructPropertyHandle]() { EVisibility Visibility = ModEnabled(); if (Visibility == EVisibility::Collapsed) { return Visibility; } uint32 NumElements = 0; ModSet->GetNumElements(NumElements); bool bIsMismatched = false; for (int32 i = 0; i < (int32)NumElements; ++i) { TSharedRef SetMemberHandle = ModSet->GetElement(i); if (ModDestinationLayoutUtils::IsParamMismatched(SetMemberHandle, StructPropertyHandle)) { bIsMismatched = true; } } return bIsMismatched ? EVisibility::Visible : EVisibility::Hidden; })); } void SetBoundsMetaData(FName FieldName, float InDefault, const TSharedRef& InHandle, TSharedRef& OutHandle) { if (InHandle->HasMetaData(FieldName)) { const FString& Value = InHandle->GetMetaData(FieldName); OutHandle->SetInstanceMetaData(FieldName, Value); } else { OutHandle->SetInstanceMetaData(FieldName, FString::Printf(TEXT("%f"), InDefault)); } } } // namespace ModParamLayoutUtils void FSoundModulationDestinationLayoutCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) { } void FSoundModulationDestinationLayoutCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { TMap> PropertyHandles; uint32 NumChildren; StructPropertyHandle->GetNumChildren(NumChildren); for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) { TSharedRef ChildHandle = StructPropertyHandle->GetChildHandle(ChildIndex).ToSharedRef(); const FName PropertyName = ChildHandle->GetProperty()->GetFName(); PropertyHandles.Add(PropertyName, ChildHandle); } TSharedRefEnablementHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDestinationSettings, bEnableModulation)).ToSharedRef(); TSharedRefModulatorsHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDestinationSettings, Modulators)).ToSharedRef(); TSharedRefValueHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDestinationSettings, Value)).ToSharedRef(); if (ModDestinationLayoutUtils::IsModulationEnabled()) { ModDestinationLayoutUtils::CustomizeChildren_AddValueRow(ChildBuilder, StructPropertyHandle, ValueHandle, ModulatorsHandle, EnablementHandle); ModDestinationLayoutUtils::CustomizeChildren_AddModulatorRow(ChildBuilder, StructPropertyHandle, ModulatorsHandle, EnablementHandle); } else { ModDestinationLayoutUtils::CustomizeChildren_AddValueNoModRow(ChildBuilder, StructPropertyHandle, ValueHandle); } } void FSoundModulationDefaultSettingsLayoutCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) { } void FSoundModulationDefaultSettingsLayoutCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { if (ModDestinationLayoutUtils::IsModulationEnabled()) { TMap> PropertyHandles; uint32 NumChildren; StructPropertyHandle->GetNumChildren(NumChildren); for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) { TSharedRef ChildHandle = StructPropertyHandle->GetChildHandle(ChildIndex).ToSharedRef(); const FName PropertyName = ChildHandle->GetProperty()->GetFName(); PropertyHandles.Add(PropertyName, ChildHandle); } TSharedRef VolumeHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultSettings, VolumeModulationDestination)).ToSharedRef(); TSharedRef PitchHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultSettings, PitchModulationDestination)).ToSharedRef(); TSharedRef HighpassHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultSettings, HighpassModulationDestination)).ToSharedRef(); TSharedRef LowpassHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultSettings, LowpassModulationDestination)).ToSharedRef(); ChildBuilder.AddProperty(VolumeHandle); // Do not add the pitch destination to source buses TArray Objects; StructPropertyHandle->GetOuterObjects(Objects); const bool bEditingSourceBus = Algo::AnyOf(Objects, [](const UObject* Object) -> bool { return Object->IsA(); }); if (!bEditingSourceBus) { ChildBuilder.AddProperty(PitchHandle); } ChildBuilder.AddProperty(HighpassHandle); ChildBuilder.AddProperty(LowpassHandle); } } void FSoundModulationDefaultRoutingSettingsLayoutCustomization::CustomizeHeader(TSharedRef StructPropertyHandle, FDetailWidgetRow& HeaderRow, IPropertyTypeCustomizationUtils& CustomizationUtils) { } void FSoundModulationDefaultRoutingSettingsLayoutCustomization::CustomizeChildren(TSharedRef StructPropertyHandle, IDetailChildrenBuilder& ChildBuilder, IPropertyTypeCustomizationUtils& StructCustomizationUtils) { if (ModDestinationLayoutUtils::IsModulationEnabled()) { TMap> PropertyHandles; uint32 NumChildren; StructPropertyHandle->GetNumChildren(NumChildren); for (uint32 ChildIndex = 0; ChildIndex < NumChildren; ++ChildIndex) { TSharedRef ChildHandle = StructPropertyHandle->GetChildHandle(ChildIndex).ToSharedRef(); const FName PropertyName = ChildHandle->GetProperty()->GetFName(); PropertyHandles.Add(PropertyName, ChildHandle); } TSharedRef VolumeRouting = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, VolumeRouting)).ToSharedRef(); TSharedRef VolumeHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, VolumeModulationDestination)).ToSharedRef(); TSharedRef PitchRouting = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, PitchRouting)).ToSharedRef(); TSharedRef PitchHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, PitchModulationDestination)).ToSharedRef(); TSharedRef HighpassRouting = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, HighpassRouting)).ToSharedRef(); TSharedRef HighpassHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, HighpassModulationDestination)).ToSharedRef(); TSharedRef LowpassRouting = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, LowpassRouting)).ToSharedRef(); TSharedRef LowpassHandle = PropertyHandles.FindChecked(GET_MEMBER_NAME_CHECKED(FSoundModulationDefaultRoutingSettings, LowpassModulationDestination)).ToSharedRef(); auto ShowModSettings = [] (TSharedRef RoutingHandle) { return TAttribute::Create([RoutingHandle]() { uint8 RoutingValue = 0; if (RoutingHandle->GetValue(RoutingValue) != FPropertyAccess::Success) { return EVisibility::Collapsed; } switch (static_cast(RoutingValue)) { case EModulationRouting::Disable: case EModulationRouting::Inherit: { return EVisibility::Collapsed; } case EModulationRouting::Override: case EModulationRouting::Union: default: { return EVisibility::Visible; } } }); }; ChildBuilder.AddProperty(VolumeRouting); ChildBuilder.AddProperty(VolumeHandle).Visibility(ShowModSettings(VolumeRouting)); // Do not add pitch destination to source buses TArray Objects; StructPropertyHandle->GetOuterObjects(Objects); const bool bEditingSourceBus = Algo::AnyOf(Objects, [](const UObject* Object) -> bool { return Object->IsA(); }); if (!bEditingSourceBus) { ChildBuilder.AddProperty(PitchRouting); ChildBuilder.AddProperty(PitchHandle).Visibility(ShowModSettings(PitchRouting)); } ChildBuilder.AddProperty(HighpassRouting); ChildBuilder.AddProperty(HighpassHandle).Visibility(ShowModSettings(HighpassRouting)); ChildBuilder.AddProperty(LowpassRouting); ChildBuilder.AddProperty(LowpassHandle).Visibility(ShowModSettings(LowpassRouting)); } } #undef LOCTEXT_NAMESPACE