// Copyright Epic Games, Inc. All Rights Reserved. #include "Sections/MovieSceneAudioSection.h" #include "Decorations/MovieSceneLanguagePreviewDecoration.h" #include "Decorations/MovieSceneSectionAnchorsDecoration.h" #include "Tracks/MovieSceneAudioTrack.h" #include "Sound/SoundBase.h" #include "UObject/SequencerObjectVersion.h" #include "Channels/MovieSceneChannelProxy.h" #include "GameFramework/Actor.h" #include "MovieScene.h" #include "MovieSceneCommonHelpers.h" #include "Misc/FrameRate.h" #include "Misc/GeneratedTypeName.h" #include "Misc/PackageName.h" #include "EntitySystem/BuiltInComponentTypes.h" #include "MovieSceneTracksComponentTypes.h" #include UE_INLINE_GENERATED_CPP_BY_NAME(MovieSceneAudioSection) #if WITH_EDITOR struct FAudioChannelEditorData { FAudioChannelEditorData() { Data[0].SetIdentifiers("Volume", NSLOCTEXT("MovieSceneAudioSection", "SoundVolumeText", "Volume")); Data[1].SetIdentifiers("Pitch", NSLOCTEXT("MovieSceneAudioSection", "PitchText", "Pitch")); Data[2].SetIdentifiers("AttachActor", NSLOCTEXT("MovieSceneAudioSection", "AttachActorText", "Attach")); } FMovieSceneChannelMetaData Data[3]; }; #endif // WITH_EDITOR namespace { float AudioDeprecatedMagicNumber = TNumericLimits::Lowest(); FFrameNumber GetStartOffsetAtTrimTime(FQualifiedFrameTime TrimTime, FFrameNumber StartOffset, FFrameNumber StartFrame) { return StartOffset + TrimTime.Time.FrameNumber - StartFrame; } } namespace UE::MovieScene { /* * Entity IDs are an encoded type and index, with the upper bit being the type (scalar iputs vs audio trigger), * and the lower 31 bits as the entity index */ enum class EAudioSectionEntityType : uint8 { MainEntity, InputsEntity, TriggerEntity }; uint32 EncodeEntityID(int32 InIndex, EAudioSectionEntityType InEntityType) { check(InIndex >= 0 && InIndex < int32(0x00FFFFFF)); return static_cast(InIndex) | ((uint8)InEntityType << 24); } void DecodeEntityID(uint32 InEntityID, int32& OutIndex, EAudioSectionEntityType& OutEntityType) { OutIndex = static_cast(InEntityID & 0x00FFFFFF); OutEntityType = (EAudioSectionEntityType)(InEntityID >> 24); } } UMovieSceneAudioSection::UMovieSceneAudioSection( const FObjectInitializer& ObjectInitializer ) : Super( ObjectInitializer ) { Sound = nullptr; StartOffset_DEPRECATED = AudioDeprecatedMagicNumber; AudioStartTime_DEPRECATED = AudioDeprecatedMagicNumber; AudioDilationFactor_DEPRECATED = AudioDeprecatedMagicNumber; AudioVolume_DEPRECATED = AudioDeprecatedMagicNumber; bLooping = true; bSuppressSubtitles = false; bOverrideAttenuation = false; BlendType = EMovieSceneBlendType::Absolute; EvalOptions.EnableAndSetCompletionMode (GetLinkerCustomVersion(FSequencerObjectVersion::GUID) < FSequencerObjectVersion::WhenFinishedDefaultsToProjectDefault ? EMovieSceneCompletionMode::RestoreState : EMovieSceneCompletionMode::ProjectDefault); SoundVolume.SetDefault(1.f); PitchMultiplier.SetDefault(1.f); } namespace MovieSceneAudioSectionPrivate { template void AddInputChannels(UMovieSceneAudioSection* InSection, FMovieSceneChannelProxyData& InChannelProxyData) { InSection->ForEachInput([&InChannelProxyData](FName InName, const ChannelType& InChannel) { #if WITH_EDITOR FMovieSceneChannelMetaData Data; FText TextName = FText::FromName(InName); Data.SetIdentifiers(FName(InName.ToString() + GetGeneratedTypeName()), TextName, TextName); InChannelProxyData.Add(const_cast(InChannel), Data, TMovieSceneExternalValue::Make()); #else //WITH_EDITOR InChannelProxyData.Add(const_cast(InChannel)); #endif //WITH_EDITOR }); } } EMovieSceneChannelProxyType UMovieSceneAudioSection::CacheChannelProxy() { // Set up the channel proxy FMovieSceneChannelProxyData Channels; UMovieSceneAudioTrack* AudioTrack = Cast(GetOuter()); UMovieScene* MovieScene = AudioTrack ? Cast(AudioTrack->GetOuter()) : nullptr; const bool bHasAttachData = MovieScene && MovieScene->ContainsTrack(*AudioTrack); #if WITH_EDITOR FAudioChannelEditorData EditorData; Channels.Add(SoundVolume, EditorData.Data[0], TMovieSceneExternalValue()); Channels.Add(PitchMultiplier, EditorData.Data[1], TMovieSceneExternalValue()); if (bHasAttachData) { Channels.Add(AttachActorData, EditorData.Data[2]); } #else Channels.Add(SoundVolume); Channels.Add(PitchMultiplier); if (bHasAttachData) { Channels.Add(AttachActorData); } #endif using namespace MovieSceneAudioSectionPrivate; SetupSoundInputParameters(Sound); AddInputChannels(this, Channels); AddInputChannels(this, Channels); AddInputChannels(this, Channels); AddInputChannels(this, Channels); AddInputChannels(this, Channels); ChannelProxy = MakeShared(MoveTemp(Channels)); return EMovieSceneChannelProxyType::Dynamic; } USoundBase* UMovieSceneAudioSection::GetPlaybackSound() const { return UMovieSceneLanguagePreviewDecoration::FindLocalizedAsset(Sound, this); } void UMovieSceneAudioSection::PopulateInitialAnchors(TMap& OutAnchors) { UMovieSceneSectionAnchorsDecoration* Decoration = FindDecoration(); if (Decoration && HasStartFrame() && HasEndFrame()) { FFrameNumber StartTime = GetInclusiveStartFrame(); OutAnchors.Add(Decoration->StartAnchor, FMovieSceneScalingAnchor{ StartTime, (GetExclusiveEndFrame() - StartTime).Value }); } } void UMovieSceneAudioSection::PopulateAnchors(TMap& OutAnchors) { UMovieSceneSectionAnchorsDecoration* Decoration = FindDecoration(); if (Decoration && HasStartFrame() && HasEndFrame()) { USoundBase* ReferenceSound = GetSound(); USoundBase* PlaybackSound = GetPlaybackSound(); if (PlaybackSound && ReferenceSound && ReferenceSound != PlaybackSound) { const UMovieScene* MovieScene = GetTypedOuter(); const float ReferenceDuration = MovieSceneHelpers::GetSoundDuration(ReferenceSound); const float PlaybackDuration = MovieSceneHelpers::GetSoundDuration(PlaybackSound); FFrameNumber StartTime = GetInclusiveStartFrame(); FFrameNumber EndTime = GetExclusiveEndFrame(); int32 SectionLength = FMath::CeilToInt32((EndTime.Value - StartTime.Value) * double(PlaybackDuration / ReferenceDuration)); OutAnchors.Add(Decoration->StartAnchor, FMovieSceneScalingAnchor{ StartTime, SectionLength }); } } } UObject* UMovieSceneAudioSection::GetSourceObject() const { return GetSound(); } void UMovieSceneAudioSection::SetupSoundInputParameters(USoundBase* InSoundBase) { // Populate with defaults. // Don't init resources when running cook, as this can trigger // registration of a MetaSound and its dependent graphs. // Those will instead be registered when the MetaSound itself is cooked (FMetasoundAssetBase::CookMetaSound) // in a way that does not deal with runtime data like this function does // Getting the default parameters and the rest of the function are // dependent on that runtime data and don't need to be cooked if (InSoundBase && !IsRunningCookCommandlet()) { InSoundBase->InitResources(); TArray DefaultParams; InSoundBase->GetAllDefaultParameters(DefaultParams); TSet OrphanedFloatInputs; Inputs_Float.GetKeys(OrphanedFloatInputs); TSet OrphanedTriggerInputs; Inputs_Trigger.GetKeys(OrphanedTriggerInputs); TSet OrphanedBoolInputs; Inputs_Bool.GetKeys(OrphanedBoolInputs); TSet OrphanedIntInputs; Inputs_Int.GetKeys(OrphanedIntInputs); TSet OrphanedStringInputs; Inputs_String.GetKeys(OrphanedStringInputs); for (const FAudioParameter& Param : DefaultParams) { switch (Param.ParamType) { case EAudioParameterType::Float: { Inputs_Float.FindOrAdd(Param.ParamName, FMovieSceneFloatChannel{}).SetDefault(Param.FloatParam); OrphanedFloatInputs.Remove(Param.ParamName); break; } case EAudioParameterType::Trigger: { Inputs_Trigger.FindOrAdd(Param.ParamName, FMovieSceneAudioTriggerChannel{}); OrphanedTriggerInputs.Remove(Param.ParamName); break; } case EAudioParameterType::Boolean: { Inputs_Bool.FindOrAdd(Param.ParamName, FMovieSceneBoolChannel{}).SetDefault(Param.BoolParam); OrphanedBoolInputs.Remove(Param.ParamName); break; } case EAudioParameterType::Integer: { Inputs_Int.FindOrAdd(Param.ParamName, FMovieSceneIntegerChannel{}).SetDefault(Param.IntParam); OrphanedIntInputs.Remove(Param.ParamName); break; } case EAudioParameterType::String: { Inputs_String.FindOrAdd(Param.ParamName, FMovieSceneStringChannel{}).SetDefault(Param.StringParam); OrphanedStringInputs.Remove(Param.ParamName); break; } default: // Not supported yet. break; } } for (const FName& Name : OrphanedFloatInputs) { Inputs_Float.Remove(Name); } for (const FName& Name : OrphanedTriggerInputs) { Inputs_Trigger.Remove(Name); } for (const FName& Name : OrphanedBoolInputs) { Inputs_Bool.Remove(Name); } for (const FName& Name : OrphanedIntInputs) { Inputs_Int.Remove(Name); } for (const FName& Name : OrphanedStringInputs) { Inputs_String.Remove(Name); } } } TOptional UMovieSceneAudioSection::GetOffsetTime() const { return TOptional(StartFrameOffset); } void UMovieSceneAudioSection::MigrateFrameTimes(FFrameRate SourceRate, FFrameRate DestinationRate) { if (StartFrameOffset.Value > 0) { FFrameNumber NewStartFrameOffset = ConvertFrameTime(FFrameTime(StartFrameOffset), SourceRate, DestinationRate).FloorToFrame(); StartFrameOffset = NewStartFrameOffset; } } void UMovieSceneAudioSection::PostLoad() { Super::PostLoad(); if (AudioDilationFactor_DEPRECATED != AudioDeprecatedMagicNumber) { PitchMultiplier.SetDefault(AudioDilationFactor_DEPRECATED); AudioDilationFactor_DEPRECATED = AudioDeprecatedMagicNumber; } if (AudioVolume_DEPRECATED != AudioDeprecatedMagicNumber) { SoundVolume.SetDefault(AudioVolume_DEPRECATED); AudioVolume_DEPRECATED = AudioDeprecatedMagicNumber; } TOptional StartOffsetToUpgrade; if (AudioStartTime_DEPRECATED != AudioDeprecatedMagicNumber) { // Previously, start time in relation to the sequence. Start time was used to calculate the offset into the // clip at the start of the section evaluation as such: Section Start Time - Start Time. if (AudioStartTime_DEPRECATED != 0.f && HasStartFrame()) { StartOffsetToUpgrade = GetInclusiveStartFrame() / GetTypedOuter()->GetTickResolution() - AudioStartTime_DEPRECATED; } AudioStartTime_DEPRECATED = AudioDeprecatedMagicNumber; } if (StartOffset_DEPRECATED != AudioDeprecatedMagicNumber) { StartOffsetToUpgrade = StartOffset_DEPRECATED; StartOffset_DEPRECATED = AudioDeprecatedMagicNumber; } FFrameRate LegacyFrameRate = GetLegacyConversionFrameRate(); if (StartOffsetToUpgrade.IsSet()) { FFrameRate DisplayRate = GetTypedOuter()->GetDisplayRate(); FFrameRate TickResolution = GetTypedOuter()->GetTickResolution(); StartFrameOffset = ConvertFrameTime(FFrameTime::FromDecimal(DisplayRate.AsDecimal() * StartOffsetToUpgrade.GetValue()), DisplayRate, TickResolution).FrameNumber; } } TOptional > UMovieSceneAudioSection::GetAutoSizeRange() const { if (!Sound) { return TRange(); } const FFrameRate FrameRate = GetTypedOuter()->GetTickResolution(); // determine initial duration // @todo Once we have infinite sections, we can remove this // @todo ^^ Why? Infinte sections would mean there's no starting time? FFrameTime DurationToUse = 1.f * FrameRate; // if all else fails, use 1 second duration // Only one-shot non-procedural sounds have a known duration if (Sound->IsOneShot() && !Sound->IsProcedurallyGenerated()) { const float SoundDuration = MovieSceneHelpers::GetSoundDuration(Sound); // This should not hit if the sound has returned true for IsOneShot() check(SoundDuration != INDEFINITELY_LOOPING_DURATION); if (SoundDuration > 0) { DurationToUse = FMath::Max(SoundDuration * FrameRate - StartFrameOffset, FFrameTime(1)); } } const int32 IFrameNumber = DurationToUse.FrameNumber.Value + static_cast(DurationToUse.GetSubFrame() + 0.5f); return TRange(GetInclusiveStartFrame(), GetInclusiveStartFrame() + IFrameNumber); } void UMovieSceneAudioSection::TrimSection(FQualifiedFrameTime TrimTime, bool bTrimLeft, bool bDeleteKeys) { SetFlags(RF_Transactional); if (TryModify()) { if (bTrimLeft) { StartFrameOffset = HasStartFrame() ? GetStartOffsetAtTrimTime(TrimTime, StartFrameOffset, GetInclusiveStartFrame()) : 0; } Super::TrimSection(TrimTime, bTrimLeft, bDeleteKeys); } } UMovieSceneSection* UMovieSceneAudioSection::SplitSection(FQualifiedFrameTime SplitTime, bool bDeleteKeys) { const FFrameNumber InitialStartFrameOffset = StartFrameOffset; const FFrameNumber NewOffset = HasStartFrame() ? GetStartOffsetAtTrimTime(SplitTime, StartFrameOffset, GetInclusiveStartFrame()) : 0; UMovieSceneSection* NewSection = Super::SplitSection(SplitTime, bDeleteKeys); if (NewSection != nullptr) { UMovieSceneAudioSection* NewAudioSection = Cast(NewSection); NewAudioSection->StartFrameOffset = NewOffset; } // Restore original offset modified by splitting StartFrameOffset = InitialStartFrameOffset; return NewSection; } void UMovieSceneAudioSection::SetSound(USoundBase* InSound) { Sound = InSound; // For a procedurally generated sound, only have the section loop if it's specifically a looping sound // this is because there's no way to determine a duration for the sound so the user has no way of // now when to end the region to avoid retriggering the one-shot procedural asset. if (Sound->IsProcedurallyGenerated()) { bLooping = !Sound->IsOneShot(); } InvalidateChannelProxy(); } USceneComponent* UMovieSceneAudioSection::GetAttachComponent(const AActor* InParentActor, const FMovieSceneActorReferenceKey& Key) const { FName AttachComponentName = Key.ComponentName; FName AttachSocketName = Key.SocketName; if (AttachSocketName != NAME_None) { if (AttachComponentName != NAME_None) { TInlineComponentArray PotentialAttachComponents(InParentActor); for (USceneComponent* PotentialAttachComponent : PotentialAttachComponents) { if (PotentialAttachComponent->GetFName() == AttachComponentName && PotentialAttachComponent->DoesSocketExist(AttachSocketName)) { return PotentialAttachComponent; } } } else if (InParentActor->GetRootComponent()->DoesSocketExist(AttachSocketName)) { return InParentActor->GetRootComponent(); } } else if (AttachComponentName != NAME_None) { TInlineComponentArray PotentialAttachComponents(InParentActor); for (USceneComponent* PotentialAttachComponent : PotentialAttachComponents) { if (PotentialAttachComponent->GetFName() == AttachComponentName) { return PotentialAttachComponent; } } } if (InParentActor->GetDefaultAttachComponent()) { return InParentActor->GetDefaultAttachComponent(); } else { return InParentActor->GetRootComponent(); } } bool UMovieSceneAudioSection::PopulateEvaluationFieldImpl(const TRange& EffectiveRange, const FMovieSceneEvaluationFieldEntityMetaData& InMetaData, FMovieSceneEntityComponentFieldBuilder* OutFieldBuilder) { using namespace UE::MovieScene; int32 MetaDataIndex = OutFieldBuilder->AddMetaData(InMetaData); // Add the default entity first. int32 MainEntityIndex = OutFieldBuilder->FindOrAddEntity(this, EncodeEntityID(0, EAudioSectionEntityType::MainEntity)); OutFieldBuilder->AddPersistentEntity(EffectiveRange, MainEntityIndex, MetaDataIndex); // See how many additional entities we need to store the audio input data. // We can pack 9 float channels per entity, but we can only have one string/bool/int/audio-trigger // channel per entity. int32 NumInputDataEntities = Inputs_Float.Num() % 9; NumInputDataEntities = FMath::Max(NumInputDataEntities, Inputs_String.Num()); NumInputDataEntities = FMath::Max(NumInputDataEntities, Inputs_Bool.Num()); NumInputDataEntities = FMath::Max(NumInputDataEntities, Inputs_Int.Num()); // Add these extra entities to the evaluation field. for (int32 InputDataEntity = 0; InputDataEntity < NumInputDataEntities; ++InputDataEntity) { int32 EntityIndex = OutFieldBuilder->FindOrAddEntity(this, EncodeEntityID(InputDataEntity, EAudioSectionEntityType::InputsEntity)); OutFieldBuilder->AddPersistentEntity(EffectiveRange, EntityIndex, MetaDataIndex); } // Audio triggers are added differently, as one-shot entities. TArray AudioTriggerNames; Inputs_Trigger.GetKeys(AudioTriggerNames); AudioTriggerNames.Sort(FNameLexicalLess()); for (int32 TriggerIndex = 0; TriggerIndex < AudioTriggerNames.Num(); ++TriggerIndex) { FMovieSceneAudioTriggerChannel& TriggerChannel = Inputs_Trigger[AudioTriggerNames[TriggerIndex]]; TArrayView Times = TriggerChannel.GetTimes(); for (int32 Index = 0; Index < Times.Num(); ++Index) { if (EffectiveRange.Contains(Times[Index])) { TRange TriggerRange(Times[Index]); int32 EntityIndex = OutFieldBuilder->FindOrAddEntity(this, EncodeEntityID(TriggerIndex, EAudioSectionEntityType::TriggerEntity)); OutFieldBuilder->AddOneShotEntity(TriggerRange, EntityIndex, MetaDataIndex); } } } // Return true to indicate we've done everything ourselves. return true; } void UMovieSceneAudioSection::ImportEntityImpl(UMovieSceneEntitySystemLinker* EntityLinker, const FEntityImportParams& Params, FImportedEntity* OutImportedEntity) { using namespace UE::MovieScene; if (!Sound) { return; } const FBuiltInComponentTypes* BuiltInComponents = FBuiltInComponentTypes::Get(); const FMovieSceneTracksComponentTypes* TrackComponents = FMovieSceneTracksComponentTypes::Get(); const FGuid ObjectBindingID = Params.GetObjectBindingID(); int32 EntityIndex; EAudioSectionEntityType EntityType; DecodeEntityID(Params.EntityID, EntityIndex, EntityType); if (EntityType == EAudioSectionEntityType::MainEntity) { // Default entity... we add the main audio component data, plus the volume and pitch channels. OutImportedEntity->AddBuilder( FEntityBuilder() .AddConditional(BuiltInComponents->GenericObjectBinding, ObjectBindingID, ObjectBindingID.IsValid()) .AddTagConditional(BuiltInComponents->Tags.Root, !ObjectBindingID.IsValid()) .Add(TrackComponents->Audio, FMovieSceneAudioComponentData{ this }) .Add(BuiltInComponents->FloatChannel[0], &SoundVolume) .Add(BuiltInComponents->FloatChannel[1], &PitchMultiplier) ); } else if (EntityType == EAudioSectionEntityType::InputsEntity) { // Additional entities for custom audio input values. TArray InputNames; FMovieSceneAudioInputData InputData; // There are up to 9 float channels per entity, and we know that all entities are fully packed // until the last one. So add as many as we can for the given entity we're building. int32 FloatInputStartIndex = EntityIndex * 9; if (FloatInputStartIndex < Inputs_Float.Num()) { int32 FloatInputNum = FMath::Min(9, Inputs_Float.Num() - FloatInputStartIndex); Inputs_Float.GetKeys(InputNames); for (int32 Offset = 0; Offset < FloatInputNum; ++Offset) { InputData.FloatInputs[Offset] = InputNames[FloatInputStartIndex + Offset]; } } // Other inputs can only be added once per entity, so add one of each type that exists. int32 OtherInputStartIndex = EntityIndex; if (OtherInputStartIndex < Inputs_String.Num()) { InputNames.Reset(); Inputs_String.GetKeys(InputNames); InputData.StringInput = InputNames[OtherInputStartIndex]; } if (OtherInputStartIndex < Inputs_Bool.Num()) { InputNames.Reset(); Inputs_Bool.GetKeys(InputNames); InputData.BoolInput = InputNames[OtherInputStartIndex]; } if (OtherInputStartIndex < Inputs_Int.Num()) { InputNames.Reset(); Inputs_Int.GetKeys(InputNames); InputData.IntInput = InputNames[OtherInputStartIndex]; } // Make this additional entity by adding the component that specifies what audio input channels // are present, plus all of these channels. OutImportedEntity->AddBuilder( FEntityBuilder() .AddConditional(BuiltInComponents->GenericObjectBinding, ObjectBindingID, ObjectBindingID.IsValid()) .AddTagConditional(BuiltInComponents->Tags.Root, !ObjectBindingID.IsValid()) .Add(TrackComponents->Audio, FMovieSceneAudioComponentData{ this }) .Add(TrackComponents->AudioInputs, InputData) ); for (int32 Index = 0; Index < 9; ++Index) { FName InputName = InputData.FloatInputs[Index]; if (!InputName.IsNone()) { OutImportedEntity->AddBuilder( FEntityBuilder() .Add(BuiltInComponents->FloatChannel[Index], &Inputs_Float[InputName]) ); } } if (!InputData.StringInput.IsNone()) { OutImportedEntity->AddBuilder( FEntityBuilder() .Add(BuiltInComponents->StringChannel, &Inputs_String[InputData.StringInput]) ); } if (!InputData.BoolInput.IsNone()) { OutImportedEntity->AddBuilder( FEntityBuilder() .Add(BuiltInComponents->BoolChannel, &Inputs_Bool[InputData.BoolInput]) ); } if (!InputData.IntInput.IsNone()) { OutImportedEntity->AddBuilder( FEntityBuilder() .Add(BuiltInComponents->IntegerChannel, &Inputs_Int[InputData.IntInput]) ); } } else if (EntityType == EAudioSectionEntityType::TriggerEntity) { // Additional one-shot entities for audio triggers. // The decoded index is the index of the name in the triggers map. TArray AudioTriggerNames; Inputs_Trigger.GetKeys(AudioTriggerNames); AudioTriggerNames.Sort(FNameLexicalLess()); OutImportedEntity->AddBuilder( FEntityBuilder() .AddConditional(BuiltInComponents->GenericObjectBinding, ObjectBindingID, ObjectBindingID.IsValid()) .AddTagConditional(BuiltInComponents->Tags.Root, !ObjectBindingID.IsValid()) .Add(TrackComponents->Audio, FMovieSceneAudioComponentData{ this }) .Add(TrackComponents->AudioTriggerName, AudioTriggerNames[EntityIndex]) ); } }