Files
UnrealEngine/Engine/Source/Editor/MovieSceneTools/Private/FCPXML/FCPXMLExport.cpp
2025-05-18 13:04:45 +08:00

1361 lines
43 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "FCPXML/FCPXMLExport.h"
#include "MovieScene.h"
#include "LevelSequence.h"
#include "Tracks/MovieSceneAudioTrack.h"
#include "Sections/MovieSceneCinematicShotSection.h"
#include "Tracks/MovieSceneCinematicShotTrack.h"
#include "MovieSceneTimeHelpers.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "UObject/MetaData.h"
#include "Logging/TokenizedMessage.h"
#include "Sound/SoundWave.h"
#include "UObject/Package.h"
#define LOCTEXT_NAMESPACE "FCPXMLExporter"
DEFINE_LOG_CATEGORY_STATIC(LogFCPXMLExporter, Log, All);
FFCPXMLExportVisitor::FFCPXMLExportVisitor(FString InSaveFilename, TSharedRef<FMovieSceneExportData> InExportData, TSharedRef<FMovieSceneTranslatorContext> InExportContext) :
FFCPXMLNodeVisitor()
, ExportData(InExportData)
, ExportContext(InExportContext)
, SaveFilePath(InSaveFilename)
{
SaveFilePath = FPaths::GetPath(InSaveFilename);
if (FPaths::IsRelative(SaveFilePath))
{
SaveFilePath = FPaths::ConvertRelativePathToFull(SaveFilePath);
}
SequenceId = 0;
MasterClipId = 0;
ClipItemId = 0;
FileId = 0;
}
FFCPXMLExportVisitor::~FFCPXMLExportVisitor() {}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLBasicNode> InBasicNode)
{
return InBasicNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLXmemlNode> InXmemlNode)
{
float EditTime = 0.f;
// Construct the FCP 7 XML structure from Sequencer class objects
if (InXmemlNode->GetChildCount() == 0)
{
if (!ConstructProjectNode(InXmemlNode))
{
return false;
}
}
/** @todo - MERGE METADATA
Merging the newly exported XML structure with pre-existing
XML metadata may be implemented here. The traversal would proceed
through the new XML structure, referring back to the metadata
XML structure to incorporate any missing or desired attributes or
elements. Alternatively, the traversal might be invoked directly on
the metadata referring back to and modifying the new XML structure.
*/
return InXmemlNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLSequenceNode> InSequenceNode)
{
return InSequenceNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLVideoNode> InVideoNode)
{
return InVideoNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLAudioNode> InAudioNode)
{
return InAudioNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLTrackNode> InTrackNode)
{
return InTrackNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLClipNode> InClipNode)
{
return InClipNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLClipItemNode> InClipItemNode)
{
return InClipItemNode->VisitChildren(*this);
}
bool FFCPXMLExportVisitor::VisitNode(TSharedRef<FFCPXMLFileNode> InFileNode)
{
return InFileNode->VisitChildren(*this);
}
/** Creates project node. */
bool FFCPXMLExportVisitor::ConstructProjectNode(TSharedRef<FFCPXMLNode> InParentNode)
{
TSharedRef<FFCPXMLNode> ProjectNode = InParentNode->CreateChildNode(TEXT("project"));
TSharedRef<FFCPXMLNode> NameNode = ProjectNode->CreateChildNode(TEXT("name"));
NameNode->SetContent(ExportData->MovieSceneData->Name + TEXT("Project"));
TSharedRef<FFCPXMLNode> ChildrenNode = ProjectNode->CreateChildNode(TEXT("children"));
if (!ConstructMasterVideoClipNodes(ChildrenNode))
{
return false;
}
if (!ConstructMasterAudioClipNodes(ChildrenNode))
{
return false;
}
if (!ConstructSequenceNode(ChildrenNode))
{
return false;
}
return true;
}
bool FFCPXMLExportVisitor::ConstructMasterVideoClipNodes(TSharedRef<FFCPXMLNode> InParentNode)
{
if (!ExportData->IsExportDataValid() || !ExportData->MovieSceneData.IsValid() || !ExportData->MovieSceneData->CinematicData.IsValid())
{
return false;
}
TSharedPtr<FMovieSceneExportCinematicData> CinematicData = ExportData->MovieSceneData->CinematicData;
if (!CinematicData.IsValid())
{
return false;
}
for (TSharedPtr<FMovieSceneExportCinematicSectionData> CinematicSection : CinematicData->CinematicSections)
{
if (!CinematicSection.IsValid())
{
continue;
}
// skip sections outside of playback range
if (CinematicSection->bWithinPlaybackRange == false)
{
CinematicSection->bEnabled = false;
continue;
}
if (!ConstructMasterClipNode(InParentNode, CinematicSection, CinematicData))
{
return false;
}
}
return true;
}
bool FFCPXMLExportVisitor::ConstructMasterAudioClipNodes(TSharedRef<FFCPXMLNode> InParentNode)
{
if (!ExportData->IsExportDataValid() || !ExportData->MovieSceneData.IsValid())
{
return false;
}
for (TSharedPtr<FMovieSceneExportAudioData> AudioData : ExportData->MovieSceneData->AudioData)
{
if (!AudioData.IsValid())
{
return false;
}
for (TSharedPtr<FMovieSceneExportAudioTrackData> AudioTrack : AudioData->AudioTracks)
{
if (!AudioTrack.IsValid())
{
return false;
}
for (TSharedPtr<FMovieSceneExportAudioSectionData> AudioSection : AudioTrack->AudioSections)
{
bool bMasterClipExists = false;
FString MasterClipName;
HasMasterClipIdName(AudioSection, MasterClipName, bMasterClipExists);
if (!bMasterClipExists)
{
ConstructMasterClipNode(InParentNode, AudioSection, AudioData);
}
}
}
}
return true;
}
bool FFCPXMLExportVisitor::ConstructMasterClipNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportCinematicSectionData> InCinematicSectionData, const TSharedPtr<FMovieSceneExportCinematicData> InCinematicData)
{
if (!InCinematicSectionData.IsValid())
{
return false;
}
int32 Duration{ 0 };
int32 StartFrame{ 0 };
int32 EndFrame{ 0 };
int32 InFrame{ 0 };
int32 OutFrame{ 0 };
FString SectionName = InCinematicSectionData->DisplayName;
GetCinematicSectionFrames(InCinematicSectionData, Duration, StartFrame, EndFrame, InFrame, OutFrame);
/** Construct a master clip id name based on the cinematic section and id */
FString MasterClipName{ TEXT("") };
GetMasterClipIdName(InCinematicSectionData, MasterClipName);
TSharedRef<FFCPXMLNode> ClipNode = InParentNode->CreateChildNode(TEXT("clip"));
ClipNode->AddAttribute(TEXT("id"), MasterClipName);
// @todo add to file's masterclip and refidmap HERE
ClipNode->CreateChildNode(TEXT("masterclipid"))->SetContent(MasterClipName);
ClipNode->CreateChildNode(TEXT("ismasterclip"))->SetContent(true);
ClipNode->CreateChildNode(TEXT("duration"))->SetContent(Duration);
if (!ConstructRateNode(ClipNode))
{
return false;
}
ClipNode->CreateChildNode(TEXT("in"))->SetContent(InFrame);
ClipNode->CreateChildNode(TEXT("out"))->SetContent(OutFrame);
ClipNode->CreateChildNode(TEXT("name"))->SetContent(SectionName);
TSharedRef<FFCPXMLNode> MediaNode = ClipNode->CreateChildNode(TEXT("media"));
TSharedRef<FFCPXMLNode> VideoNode = MediaNode->CreateChildNode(TEXT("video"));
TSharedRef<FFCPXMLNode> TrackNode = VideoNode->CreateChildNode(TEXT("track"));
if (!ConstructVideoClipItemNode(TrackNode, InCinematicSectionData, InCinematicData, true))
{
return false;
}
if (!ConstructLoggingInfoNode(ClipNode, InCinematicSectionData->MovieSceneSection))
{
return false;
}
if (!ConstructColorInfoNode(ClipNode))
{
return false;
}
return true;
}
bool FFCPXMLExportVisitor::ConstructMasterClipNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportAudioSectionData> InAudioSectionData, const TSharedPtr<FMovieSceneExportAudioData> InAudioData)
{
if (!InAudioSectionData.IsValid())
{
return false;
}
bool bIsStereo = (InAudioSectionData->NumChannels == 2);
int32 Duration{ 0 };
int32 StartFrame{ 0 };
int32 EndFrame{ 0 };
int32 InFrame{ 0 };
int32 OutFrame{ 0 };
FString SectionName = InAudioSectionData->DisplayName;
GetAudioSectionFrames(InAudioSectionData, Duration, StartFrame, EndFrame, InFrame, OutFrame);
/** Construct a master clip id name based on the audio section and id */
FString MasterClipName{ TEXT("") };
GetMasterClipIdName(InAudioSectionData, MasterClipName);
TSharedRef<FFCPXMLNode> ClipNode = InParentNode->CreateChildNode(TEXT("clip"));
ClipNode->AddAttribute(TEXT("id"), MasterClipName);
ClipNode->AddAttribute(TEXT("explodedTracks"), TEXT("true"));
// @todo add to file's masterclip and refidmap HERE
ClipNode->CreateChildNode(TEXT("masterclipid"))->SetContent(MasterClipName);
ClipNode->CreateChildNode(TEXT("ismasterclip"))->SetContent(true);
ClipNode->CreateChildNode(TEXT("duration"))->SetContent(Duration);
if (!ConstructRateNode(ClipNode))
{
return false;
}
ClipNode->CreateChildNode(TEXT("in"))->SetContent(InFrame);
ClipNode->CreateChildNode(TEXT("out"))->SetContent(OutFrame);
ClipNode->CreateChildNode(TEXT("name"))->SetContent(SectionName);
TSharedRef<FFCPXMLNode> MediaNode = ClipNode->CreateChildNode(TEXT("media"));
TSharedRef<FFCPXMLNode> AudioNode = MediaNode->CreateChildNode(TEXT("audio"));
TSharedRef<FFCPXMLNode> TrackNode = AudioNode->CreateChildNode(TEXT("track"));
FString ClipItemIdName1{ TEXT("") };
FString ClipItemIdName2{ TEXT("") };
GetNextClipItemIdName(ClipItemIdName1);
if (bIsStereo)
{
GetNextClipItemIdName(ClipItemIdName2);
}
if (!ConstructAudioClipItemNode(TrackNode, InAudioSectionData, InAudioData, 1, true, ClipItemIdName1, ClipItemIdName2, 1, 1, 1, 2))
{
return false;
}
// handle stereo master clip
if (bIsStereo)
{
TrackNode = AudioNode->CreateChildNode(TEXT("track"));
if (!ConstructAudioClipItemNode(TrackNode, InAudioSectionData, InAudioData, 2, true, ClipItemIdName1, ClipItemIdName2, 1, 1, 1, 2))
{
return false;
}
}
if (!ConstructLoggingInfoNode(ClipNode, InAudioSectionData))
{
return false;
}
return true;
}
/** Creates logginginfo node. */
bool FFCPXMLExportVisitor::ConstructLoggingInfoNode(TSharedRef<FFCPXMLNode> InParentNode, const UMovieSceneSection* InMovieSceneSection)
{
if (!IsValid(InMovieSceneSection))
{
return false;
}
TSharedRef<FFCPXMLNode> LoggingInfoNode = InParentNode->CreateChildNode(TEXT("logginginfo"));
ConstructLoggingInfoElements(LoggingInfoNode, InMovieSceneSection);
TSharedPtr<FFCPXMLNode> LogNoteNode = LoggingInfoNode->GetChildNode(TEXT("lognote"), ENodeInherit::NoInherit, ENodeReference::NoReferences);
if (!LogNoteNode.IsValid())
{
LogNoteNode = LoggingInfoNode->CreateChildNode(TEXT("lognote"));
}
FString Metadata{ TEXT("") };
const UMovieSceneCinematicShotSection* ShotSection = Cast<UMovieSceneCinematicShotSection>(InMovieSceneSection);
if (ShotSection == nullptr)
{
return false;
}
if (!CreateCinematicSectionMetadata(ShotSection, Metadata))
{
return false;
}
LogNoteNode->SetContent(Metadata);
return true;
}
/** Creates logginginfo node. */
bool FFCPXMLExportVisitor::ConstructLoggingInfoNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportAudioSectionData> InSectionData)
{
if (!ExportData->MovieSceneData.IsValid() || !InSectionData.IsValid() || InSectionData->MovieSceneSection == nullptr)
{
return false;
}
const UMovieSceneAudioSection* AudioSection = Cast<UMovieSceneAudioSection>(InSectionData->MovieSceneSection);
if (AudioSection == nullptr)
{
return false;
}
USoundBase *Sound = AudioSection->GetSound();
if (Sound == nullptr)
{
// skip logging
return true;
}
USoundWave* SoundWave = Cast<USoundWave>(Sound);
if (SoundWave == nullptr)
{
// skip logging
return true;
}
TSharedRef<FFCPXMLNode> LoggingInfoNode = InParentNode->CreateChildNode(TEXT("logginginfo"));
ConstructLoggingInfoElements(LoggingInfoNode, InSectionData->MovieSceneSection);
TSharedPtr<FFCPXMLNode> LogNoteNode = LoggingInfoNode->GetChildNode(TEXT("lognote"), ENodeInherit::NoInherit, ENodeReference::NoReferences);
if (!LogNoteNode.IsValid())
{
LogNoteNode = LoggingInfoNode->CreateChildNode(TEXT("lognote"));
}
TArray <TSharedPtr<FMovieSceneExportAudioSectionData> > AudioSectionsData;
ExportData->FindAudioSections(SoundWave->GetPathName(), AudioSectionsData);
TArray< const UMovieSceneAudioSection*> AudioSections;
for (TSharedPtr<FMovieSceneExportAudioSectionData> AudioSectionData : AudioSectionsData)
{
if (AudioSectionData.IsValid() && AudioSectionData->MovieSceneSection != nullptr)
{
const UMovieSceneAudioSection* Section = Cast<UMovieSceneAudioSection>(AudioSectionData->MovieSceneSection);
if (Section != nullptr && Section->GetSound() != nullptr)
{
if (Section->GetSound()->GetPathName() == SoundWave->GetPathName())
{
AudioSections.Add(Section);
}
}
}
}
FString Metadata{ TEXT("") };
if (!CreateSoundWaveMetadata(SoundWave, AudioSections, Metadata))
{
return false;
}
LogNoteNode->SetContent(Metadata);
return true;
}
/** Creates logginginfo elements. */
void FFCPXMLExportVisitor::ConstructLoggingInfoElements(TSharedRef<FFCPXMLNode> InLoggingInfoNode, const UObject* InObject)
{
TSharedRef<FFCPXMLNode> DescriptionNode = InLoggingInfoNode->CreateChildNode(TEXT("description"));
TSharedRef<FFCPXMLNode> SceneNode = InLoggingInfoNode->CreateChildNode(TEXT("scene"));
TSharedRef<FFCPXMLNode> ShotTakeNode = InLoggingInfoNode->CreateChildNode(TEXT("shottake"));
TSharedRef<FFCPXMLNode> GoodNode = InLoggingInfoNode->CreateChildNode(TEXT("good"));
TSharedRef<FFCPXMLNode> OriginalVideoNode = InLoggingInfoNode->CreateChildNode(TEXT("originalvideofilename"));
TSharedRef<FFCPXMLNode> OriginalAudioNode = InLoggingInfoNode->CreateChildNode(TEXT("originalaudiofilename"));
if (InObject != nullptr)
{
SetLoggingInfoElementValue(DescriptionNode, InObject, TEXT("description"));
SetLoggingInfoElementValue(SceneNode, InObject, TEXT("scene"));
SetLoggingInfoElementValue(ShotTakeNode, InObject, TEXT("shottake"));
SetLoggingInfoElementValue(GoodNode, InObject, TEXT("good"));
SetLoggingInfoElementValue(OriginalVideoNode, InObject, TEXT("originalvideofilename"));
SetLoggingInfoElementValue(OriginalAudioNode, InObject, TEXT("originalaudiofilename"));
}
}
/** Set logginginfo element value. */
void FFCPXMLExportVisitor::SetLoggingInfoElementValue(TSharedPtr<FFCPXMLNode> InNode, const UObject* InObject, const FString& InElement)
{
if (InNode.IsValid() && InObject != nullptr)
{
UPackage* Package = InObject->GetOutermost();
check(Package);
FMetaData& MetaData = Package->GetMetaData();
if (MetaData.HasValue(InObject, *InElement))
{
FString Value = MetaData.GetValue(InObject, *InElement);
InNode->SetContent(Value);
}
}
}
/** Creates colorinfo node. */
bool FFCPXMLExportVisitor::ConstructColorInfoNode(TSharedRef<FFCPXMLNode> InParentNode)
{
TSharedPtr<FFCPXMLNode> ColorInfoNode = InParentNode->CreateChildNode(TEXT("colorinfo"));
ColorInfoNode->CreateChildNode(TEXT("lut"));
ColorInfoNode->CreateChildNode(TEXT("lut1"));
ColorInfoNode->CreateChildNode(TEXT("asc_sop"));
ColorInfoNode->CreateChildNode(TEXT("asc_sat"));
ColorInfoNode->CreateChildNode(TEXT("lut2"));
return true;
}
bool FFCPXMLExportVisitor::ConstructSequenceNode(TSharedRef<FFCPXMLNode> InParentNode)
{
if (!ExportData->IsExportDataValid() || !ExportData->MovieSceneData.IsValid())
{
return false;
}
TSharedRef<FFCPXMLNode> SequenceNode = InParentNode->CreateChildNode("sequence");
// attributes
SequenceNode->AddAttribute(TEXT("id"), FString::Printf(TEXT("sequence-%d"), ++SequenceId));
// required elements
TSharedRef<FFCPXMLNode> DurationNode = SequenceNode->CreateChildNode(TEXT("duration"));
DurationNode->SetContent(ExportData->MovieSceneData->Duration);
if (!ConstructRateNode(SequenceNode))
{
return false;
}
TSharedRef<FFCPXMLNode> NameNode = SequenceNode->CreateChildNode(TEXT("name"));
NameNode->SetContent(ExportData->MovieSceneData->Name);
TSharedRef<FFCPXMLNode> MediaNode = SequenceNode->CreateChildNode(TEXT("media"));
if (!ConstructVideoNode(MediaNode))
{
return false;
}
if (!ConstructAudioNode(MediaNode))
{
return false;
}
if (!ConstructTimecodeNode(SequenceNode))
{
return false;
}
return true;
}
bool FFCPXMLExportVisitor::ConstructVideoNode(TSharedRef<FFCPXMLNode> InParentNode)
{
if (!ExportData->IsExportDataValid() || !ExportData->MovieSceneData.IsValid() || !ExportData->MovieSceneData->CinematicData.IsValid())
{
return false;
}
TSharedPtr<FMovieSceneExportCinematicData> CinematicData = ExportData->MovieSceneData->CinematicData;
if (!CinematicData.IsValid())
{
return false;
}
TSharedRef<FFCPXMLNode> VideoNode = InParentNode->CreateChildNode(TEXT("video"));
TSharedRef<FFCPXMLNode> FormatNode = VideoNode->CreateChildNode(TEXT("format"));
if (!ConstructVideoSampleCharacteristicsNode(FormatNode, ExportData->GetResX(), ExportData->GetResY()))
{
return false;
}
// Add in reverse order
for (int32 RowIndex = CinematicData->CinematicTracks.Num()-1; RowIndex >= 0; --RowIndex)
{
if (!ConstructVideoTrackNode(VideoNode, CinematicData->CinematicTracks[RowIndex], CinematicData))
{
return false;
}
}
return true;
}
bool FFCPXMLExportVisitor::ConstructAudioNode(TSharedRef<FFCPXMLNode> InParentNode)
{
if (!ExportData->IsExportDataValid())
{
return false;
}
TSharedRef<FFCPXMLNode> AudioNode = InParentNode->CreateChildNode(TEXT("audio"));
int32 NumChannels = 1;
for (TSharedPtr<FMovieSceneExportAudioData> AudioData : ExportData->MovieSceneData->AudioData)
{
if (HasStereoAudioSections(AudioData->AudioSections))
{
NumChannels = 2;
break;
}
}
AudioNode->CreateChildNode(TEXT("numOutputChannels"))->SetContent(NumChannels);
TSharedRef<FFCPXMLNode> FormatNode = AudioNode->CreateChildNode(TEXT("format"));
if (!ConstructAudioSampleCharacteristicsNode(FormatNode, ExportData->GetDefaultAudioDepth(), ExportData->GetDefaultAudioSampleRate()))
{
return false;
}
int32 Downmix = 0;
TSharedRef<FFCPXMLNode> OutputsNode = AudioNode->CreateChildNode(TEXT("outputs"));
TSharedRef<FFCPXMLNode> GroupNode = OutputsNode->CreateChildNode(TEXT("group"));
GroupNode->CreateChildNode(TEXT("index"))->SetContent(1);
GroupNode->CreateChildNode(TEXT("numchannels"))->SetContent(1);
GroupNode->CreateChildNode(TEXT("downmix"))->SetContent(Downmix);
TSharedRef<FFCPXMLNode> ChannelNode = GroupNode->CreateChildNode(TEXT("channel"));
ChannelNode->CreateChildNode(TEXT("index"))->SetContent(1);
if (NumChannels == 2)
{
GroupNode = OutputsNode->CreateChildNode(TEXT("group"));
GroupNode->CreateChildNode(TEXT("index"))->SetContent(2);
GroupNode->CreateChildNode(TEXT("numchannels"))->SetContent(1);
GroupNode->CreateChildNode(TEXT("downmix"))->SetContent(Downmix);
ChannelNode = GroupNode->CreateChildNode(TEXT("channel"));
ChannelNode->CreateChildNode(TEXT("index"))->SetContent(2);
}
uint32 TrackIndex = 1;
// Add in reverse order
for (int32 RowIndex = ExportData->MovieSceneData->AudioData.Num() - 1; RowIndex >= 0; --RowIndex)
{
if (!ExportData->MovieSceneData->AudioData[RowIndex].IsValid())
{
return false;
}
for (TSharedPtr<FMovieSceneExportAudioTrackData> AudioTrack : ExportData->MovieSceneData->AudioData[RowIndex]->AudioTracks)
{
uint32 OutNumTracks{ 0 };
if (!ConstructAudioTrackNode(AudioNode, AudioTrack, ExportData->MovieSceneData->AudioData[RowIndex], TrackIndex, OutNumTracks))
{
return false;
}
TrackIndex += OutNumTracks;
}
}
return true;
}
bool FFCPXMLExportVisitor::ConstructVideoTrackNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportCinematicTrackData> InCinematicTrackData, const TSharedPtr<FMovieSceneExportCinematicData> InCinematicData)
{
if (!ExportData->IsExportDataValid() || !InCinematicTrackData.IsValid())
{
return false;
}
TSharedRef<FFCPXMLNode> TrackNode = InParentNode->CreateChildNode(TEXT("track"));
for (TSharedPtr<FMovieSceneExportCinematicSectionData> CinematicSection : InCinematicTrackData->CinematicSections)
{
// skip disabled sections
if (!CinematicSection.IsValid() || !CinematicSection->bEnabled)
{
continue;
}
if (!ConstructVideoClipItemNode(TrackNode, CinematicSection, InCinematicData, false))
{
return false;
}
}
TSharedRef<FFCPXMLNode> EnabledNode = TrackNode->CreateChildNode(TEXT("enabled"));
EnabledNode->SetContent(true);
TSharedRef<FFCPXMLNode> LockedNode = TrackNode->CreateChildNode(TEXT("locked"));
LockedNode->SetContent(false);
return true;
}
bool FFCPXMLExportVisitor::HasStereoAudioSections(const TArray<TSharedPtr<FMovieSceneExportAudioSectionData>>& InAudioSections) const
{
for (TSharedPtr<FMovieSceneExportAudioSectionData> AudioSection : InAudioSections)
{
if (AudioSection.IsValid() && AudioSection->NumChannels == 2)
{
return true;
}
}
return false;
}
bool FFCPXMLExportVisitor::ConstructAudioTrackNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportAudioTrackData> InAudioTrackData, const TSharedPtr<FMovieSceneExportAudioData> InAudioData, uint32 InTrackIndex, uint32 OutNumTracks)
{
if (!ExportData->IsExportDataValid() || !InAudioTrackData.IsValid())
{
return false;
}
bool bTrackHasStereoClips = HasStereoAudioSections(InAudioTrackData->AudioSections);
int32 TrackIndex1{ 0 };
int32 TrackIndex2{ 0 };
if (bTrackHasStereoClips)
{
OutNumTracks = 2;
TrackIndex1 = InTrackIndex;
TrackIndex2 = InTrackIndex + 1;
}
else
{
OutNumTracks = 1;
TrackIndex1 = InTrackIndex;
TrackIndex2 = InTrackIndex;
}
// Generate all clipitem names for this track so that linked clipitems can be associated
FString ClipItemIdName{ TEXT("") };
TArray<FString> ClipItem1;
TArray<FString> ClipItem2;
TArray<int32> ClipIndex1;
TArray<int32> ClipIndex2;
int32 Index = 0;
for (TSharedPtr<FMovieSceneExportAudioSectionData> AudioSection : InAudioTrackData->AudioSections)
{
if (AudioSection->NumChannels < 1)
{
ExportContext->AddMessage(EMessageSeverity::Warning,
FText::Format(LOCTEXT("FCPXMLAudioChannelsInvalidWarning", "FCP XML export only supports mono or stereo audio. Skipping audio section '{0}' which an invalid number of channels: '{1}'."),
FText::FromString(AudioSection->DisplayName),
FText::FromString(FString::FromInt(AudioSection->NumChannels))));
continue;
}
else if (AudioSection->NumChannels > 2)
{
ExportContext->AddMessage(EMessageSeverity::Warning,
FText::Format(LOCTEXT("FCPXMLAudioChannelsUnsupportedWarning", "FCP XML export only supports mono or stereo audio. Skipping audio section '{0}' which has '{1}' channels."),
FText::FromString(AudioSection->DisplayName),
FText::FromString(FString::FromInt(AudioSection->NumChannels))));
continue;
}
GetNextClipItemIdName(ClipItemIdName);
ClipItem1.Add(ClipItemIdName);
ClipIndex1.Add(++Index);
}
Index = 0;
for (TSharedPtr<FMovieSceneExportAudioSectionData> AudioSection : InAudioTrackData->AudioSections)
{
if (AudioSection->NumChannels == 1)
{
ClipItem2.Add(FString(TEXT("")));
ClipIndex2.Add(-1);
}
else if (AudioSection->NumChannels == 2)
{
GetNextClipItemIdName(ClipItemIdName);
ClipItem2.Add(ClipItemIdName);
ClipIndex2.Add(++Index);
}
}
// construct track 1
TSharedRef<FFCPXMLNode> TrackNode = InParentNode->CreateChildNode(TEXT("track"));
TrackNode->AddAttribute(TEXT("currentExplodedTrackIndex"), TEXT("0"));
TrackNode->AddAttribute(TEXT("totalExplodedTrackCount"), bTrackHasStereoClips ? TEXT("2") : TEXT("1"));
TrackNode->AddAttribute(TEXT("premiereTrackType"), bTrackHasStereoClips ? TEXT("Stereo") : TEXT("Mono"));
Index = 0;
for (TSharedPtr<FMovieSceneExportAudioSectionData> AudioSection : InAudioTrackData->AudioSections)
{
if (AudioSection->NumChannels < 1 || AudioSection->NumChannels > 2)
{
continue;
}
if (!ConstructAudioClipItemNode(TrackNode, AudioSection, InAudioData, 1, false, ClipItem1[Index], ClipItem2[Index], ClipIndex1[Index], ClipIndex2[Index], TrackIndex1, TrackIndex2))
{
return false;
}
Index++;
}
TSharedRef<FFCPXMLNode> EnabledNode = TrackNode->CreateChildNode(TEXT("enabled"));
EnabledNode->SetContent(true);
TSharedRef<FFCPXMLNode> LockedNode = TrackNode->CreateChildNode(TEXT("locked"));
LockedNode->SetContent(false);
// construct track 2, if stereo clipitems exist
if (bTrackHasStereoClips)
{
TrackNode = InParentNode->CreateChildNode(TEXT("track"));
TrackNode->AddAttribute(TEXT("currentExplodedTrackIndex"), TEXT("1"));
TrackNode->AddAttribute(TEXT("totalExplodedTrackCount"), TEXT("2"));
TrackNode->AddAttribute(TEXT("premiereTrackType"), TEXT("Stereo"));
Index = 0;
for (TSharedPtr<FMovieSceneExportAudioSectionData> AudioSection : InAudioTrackData->AudioSections)
{
if (AudioSection->NumChannels < 1 || AudioSection->NumChannels > 2)
{
continue;
}
if (AudioSection->NumChannels == 2)
{
if (!ConstructAudioClipItemNode(TrackNode, AudioSection, InAudioData, 2, false, ClipItem1[Index], ClipItem2[Index], ClipIndex1[Index], ClipIndex2[Index], TrackIndex1, TrackIndex2))
{
return false;
}
}
Index++;
}
EnabledNode = TrackNode->CreateChildNode(TEXT("enabled"));
EnabledNode->SetContent(true);
LockedNode = TrackNode->CreateChildNode(TEXT("locked"));
LockedNode->SetContent(false);
}
return true;
}
bool FFCPXMLExportVisitor::ConstructVideoClipItemNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportCinematicSectionData> InCinematicSectionData, const TSharedPtr<FMovieSceneExportCinematicData> InCinematicData, bool bInMasterClip)
{
if (!ExportData->IsExportDataValid() || !InCinematicSectionData.IsValid())
{
return false;
}
TSharedRef<FFCPXMLNode> ClipItemNode = InParentNode->CreateChildNode(TEXT("clipitem"));
int32 Duration{ 0 };
int32 In{ 0 };
int32 Out{ 0 };
int32 Start{ 0 };
int32 End{ 0 };
GetCinematicSectionFrames(InCinematicSectionData, Duration, Start, End, In, Out);
FString MasterClipIdName = TEXT("");
GetMasterClipIdName(InCinematicSectionData, MasterClipIdName);
FString ClipItemIdName{ TEXT("") };
GetNextClipItemIdName(ClipItemIdName);
// attributes
ClipItemNode->AddAttribute(TEXT("id"), ClipItemIdName);
// elements
ClipItemNode->CreateChildNode(TEXT("masterclipid"))->SetContent(MasterClipIdName);
ClipItemNode->CreateChildNode(TEXT("ismasterclip"))->SetContent(bInMasterClip);
ClipItemNode->CreateChildNode(TEXT("name"))->SetContent(InCinematicSectionData->DisplayName);
ClipItemNode->CreateChildNode(TEXT("enabled"))->SetContent(true);
ClipItemNode->CreateChildNode(TEXT("duration"))->SetContent(Duration);
if (!ConstructRateNode(ClipItemNode))
{
return false;
}
if (!bInMasterClip)
{
ClipItemNode->CreateChildNode(TEXT("start"))->SetContent(Start);
ClipItemNode->CreateChildNode(TEXT("end"))->SetContent(End);
}
ClipItemNode->CreateChildNode(TEXT("in"))->SetContent(In);
ClipItemNode->CreateChildNode(TEXT("out"))->SetContent(Out);
if (bInMasterClip)
{
ClipItemNode->CreateChildNode(TEXT("anamorphic"))->SetContent(false);
ClipItemNode->CreateChildNode(TEXT("pixelaspectratio"))->SetContent(FString(TEXT("square")));
ClipItemNode->CreateChildNode(TEXT("fielddominance"))->SetContent(FString(TEXT("lower")));
}
if (!ConstructVideoFileNode(ClipItemNode, InCinematicSectionData, Duration, bInMasterClip))
{
return false;
}
return true;
}
bool FFCPXMLExportVisitor::ConstructAudioClipItemNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportAudioSectionData> InAudioSectionData,
const TSharedPtr<FMovieSceneExportAudioData> InAudioData, int32 InChannel, bool bInMasterClip,
const FString& InClipItemIdName1, const FString& InClipItemIdName2, int32 InClipIndex1, int32 InClipIndex2, int32 InTrackIndex1, int32 InTrackIndex2)
{
if (!ExportData->IsExportDataValid() || !InAudioSectionData.IsValid())
{
return false;
}
bool bIsStereo = (InAudioSectionData->NumChannels == 2);
int32 Duration{ 0 };
int32 In{ 0 };
int32 Out{ 0 };
int32 Start{ 0 };
int32 End{ 0 };
GetAudioSectionFrames(InAudioSectionData, Duration, Start, End, In, Out);
FString MasterClipIdName;
GetMasterClipIdName(InAudioSectionData, MasterClipIdName);
FString ClipItemIdName = (InChannel == 1 ? InClipItemIdName1 : InClipItemIdName2);
TSharedRef<FFCPXMLNode> ClipItemNode = InParentNode->CreateChildNode(TEXT("clipitem"));
ClipItemNode->AddAttribute(TEXT("id"), ClipItemIdName);
if (!bInMasterClip)
{
ClipItemNode->AddAttribute(TEXT("premiereChannelType"), InAudioSectionData->NumChannels == 2 ? TEXT("stereo") : TEXT("mono"));
}
// elements
ClipItemNode->CreateChildNode(TEXT("masterclipid"))->SetContent(MasterClipIdName);
ClipItemNode->CreateChildNode(TEXT("name"))->SetContent(InAudioSectionData->DisplayName);
if (!bInMasterClip)
{
ClipItemNode->CreateChildNode(TEXT("enabled"))->SetContent(true);
ClipItemNode->CreateChildNode(TEXT("duration"))->SetContent(Duration);
}
if (!ConstructRateNode(ClipItemNode))
{
return false;
}
if (!bInMasterClip)
{
ClipItemNode->CreateChildNode(TEXT("start"))->SetContent(Start);
ClipItemNode->CreateChildNode(TEXT("end"))->SetContent(End);
ClipItemNode->CreateChildNode(TEXT("in"))->SetContent(In);
ClipItemNode->CreateChildNode(TEXT("out"))->SetContent(Out);
}
if (!ConstructAudioFileNode(ClipItemNode, InAudioSectionData, InChannel))
{
return false;
}
TSharedRef<FFCPXMLNode> SourceTrackNode = ClipItemNode->CreateChildNode(TEXT("sourcetrack"));
SourceTrackNode->CreateChildNode(TEXT("mediatype"))->SetContent(FString(TEXT("audio")));
SourceTrackNode->CreateChildNode(TEXT("trackindex"))->SetContent(InChannel);
// stereo track clipitems must be linked using the linkclipref element
if (bIsStereo)
{
TSharedRef<FFCPXMLNode> LinkNode = ClipItemNode->CreateChildNode(TEXT("link"));
LinkNode->CreateChildNode(TEXT("linkclipref"))->SetContent(InClipItemIdName1);
LinkNode->CreateChildNode(TEXT("mediatype"))->SetContent(FString(TEXT("audio")));
LinkNode->CreateChildNode(TEXT("trackindex"))->SetContent(InTrackIndex1);
LinkNode->CreateChildNode(TEXT("clipindex"))->SetContent(InClipIndex1);
LinkNode->CreateChildNode(TEXT("groupindex"))->SetContent(1);
LinkNode = ClipItemNode->CreateChildNode(TEXT("link"));
LinkNode->CreateChildNode(TEXT("linkclipref"))->SetContent(InClipItemIdName2);
LinkNode->CreateChildNode(TEXT("mediatype"))->SetContent(FString(TEXT("audio")));
LinkNode->CreateChildNode(TEXT("trackindex"))->SetContent(InTrackIndex2);
LinkNode->CreateChildNode(TEXT("clipindex"))->SetContent(InClipIndex1);
LinkNode->CreateChildNode(TEXT("groupindex"))->SetContent(1);
}
return true;
}
bool FFCPXMLExportVisitor::ConstructVideoFileNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportCinematicSectionData> InCinematicSectionData, int32 Duration, bool bInMasterClip)
{
if (!ExportData->IsExportDataValid() || !InCinematicSectionData.IsValid())
{
return false;
}
FString FileIdName{ TEXT("") };
bool bFileExists = false;
GetFileIdName(InCinematicSectionData, FileIdName, bFileExists);
// attributes
TSharedRef<FFCPXMLNode> FileNode = InParentNode->CreateChildNode(TEXT("file"));
FileNode->AddAttribute(TEXT("id"), FileIdName);
if (!bFileExists)
{
FString FilePath = InCinematicSectionData->SourceFilePath.IsEmpty() ? SaveFilePath : InCinematicSectionData->SourceFilePath;
FString FilePathName = SaveFilePath + TEXT("/") + InCinematicSectionData->SourceFilename;
FString FilePathUrl = FString(TEXT("file://localhost/")) + FilePathName.Replace(TEXT(" "), TEXT("%20")).Replace(TEXT(":"), TEXT("%3a"));
// required elements
TSharedRef<FFCPXMLNode> NameNode = FileNode->CreateChildNode(TEXT("name"));
NameNode->SetContent(InCinematicSectionData->SourceFilename);
TSharedRef<FFCPXMLNode> PathUrlNode = FileNode->CreateChildNode(TEXT("pathurl"));
PathUrlNode->SetContent(FilePathUrl);
if (!ConstructRateNode(FileNode))
{
return false;
}
TSharedRef<FFCPXMLNode> DurationNode = FileNode->CreateChildNode(TEXT("duration"));
DurationNode->SetContent(static_cast<int32>(Duration));
if (!ConstructTimecodeNode(FileNode))
{
return false;
}
TSharedRef<FFCPXMLNode> MediaNode = FileNode->CreateChildNode(TEXT("media"));
TSharedRef<FFCPXMLNode> VideoNode = MediaNode->CreateChildNode(TEXT("video"));
if (!ConstructVideoSampleCharacteristicsNode(VideoNode, ExportData->GetResX(), ExportData->GetResY()))
{
return false;
}
}
return true;
}
bool FFCPXMLExportVisitor::ConstructAudioFileNode(TSharedRef<FFCPXMLNode> InParentNode, const TSharedPtr<FMovieSceneExportAudioSectionData> InAudioSectionData, int32 InChannel)
{
FString FileIdName{ TEXT("") };
bool bFileExists = false;
GetFileIdName(InAudioSectionData, FileIdName, bFileExists);
int32 Duration{ 0 };
int32 In{ 0 };
int32 Out{ 0 };
int32 Start{ 0 };
int32 End{ 0 };
GetAudioSectionFrames(InAudioSectionData, Duration, Start, End, In, Out);
// attributes
TSharedRef<FFCPXMLNode> FileNode = InParentNode->CreateChildNode(TEXT("file"));
FileNode->AddAttribute(TEXT("id"), FileIdName);
// only add details if file id did not already exist
if (!bFileExists)
{
// FPaths
FString FilePathName = InAudioSectionData->SourceFilePath + TEXT("/") + InAudioSectionData->SourceFilename;
FString FilePathUrl = FString(TEXT("file://localhost/")) + FilePathName.Replace(TEXT(" "), TEXT("%20")).Replace(TEXT(":"), TEXT("%3a"));
// required elements
TSharedRef<FFCPXMLNode> NameNode = FileNode->CreateChildNode(TEXT("name"));
NameNode->SetContent(InAudioSectionData->SourceFilename);
TSharedRef<FFCPXMLNode> PathUrlNode = FileNode->CreateChildNode(TEXT("pathurl"));
PathUrlNode->SetContent(FilePathUrl);
if (!ConstructRateNode(FileNode))
{
return false;
}
if (!ConstructTimecodeNode(FileNode))
{
return false;
}
FileNode->CreateChildNode(TEXT("duration"))->SetContent(static_cast<int32>(Duration));
TSharedRef<FFCPXMLNode> MediaNode = FileNode->CreateChildNode(TEXT("media"));
TSharedRef<FFCPXMLNode> AudioNode = MediaNode->CreateChildNode(TEXT("audio"));
if (!ConstructAudioSampleCharacteristicsNode(AudioNode, InAudioSectionData->Depth, InAudioSectionData->SampleRate))
{
return false;
}
AudioNode->CreateChildNode(TEXT("channelcount"))->SetContent(1);
if (InAudioSectionData->NumChannels == 2)
{
AudioNode->CreateChildNode(TEXT("layout"))->SetContent(TEXT("stereo"));
TSharedRef<FFCPXMLNode> AudioChannelNode = AudioNode->CreateChildNode(TEXT("audiochannel"));
AudioChannelNode->CreateChildNode(TEXT("sourcechannel"))->SetContent(1);
AudioChannelNode->CreateChildNode(TEXT("channellabel"))->SetContent(FString(TEXT("left")));
// second audio channel
AudioNode = MediaNode->CreateChildNode(TEXT("audio"));
if (!ConstructAudioSampleCharacteristicsNode(AudioNode, InAudioSectionData->Depth, InAudioSectionData->SampleRate))
{
return false;
}
AudioNode->CreateChildNode(TEXT("channelcount"))->SetContent(1);
AudioNode->CreateChildNode(TEXT("layout"))->SetContent(TEXT("stereo"));
AudioChannelNode = AudioNode->CreateChildNode(TEXT("audiochannel"));
AudioChannelNode->CreateChildNode(TEXT("sourcechannel"))->SetContent(2);
AudioChannelNode->CreateChildNode(TEXT("channellabel"))->SetContent(FString(TEXT("right")));
}
}
return true;
}
bool FFCPXMLExportVisitor::ConstructVideoSampleCharacteristicsNode(TSharedRef<FFCPXMLNode> InParentNode, int InWidth, int InHeight)
{
TSharedRef<FFCPXMLNode> SampleCharacteristicsNode = InParentNode->CreateChildNode(TEXT("samplecharacteristics"));
if (!ConstructRateNode(SampleCharacteristicsNode))
{
return false;
}
SampleCharacteristicsNode->CreateChildNode(TEXT("width"))->SetContent(InWidth);
SampleCharacteristicsNode->CreateChildNode(TEXT("height"))->SetContent(InHeight);
SampleCharacteristicsNode->CreateChildNode(TEXT("anamorphic"))->SetContent(false);
SampleCharacteristicsNode->CreateChildNode(TEXT("pixelaspectratio"))->SetContent(FString(TEXT("square")));
SampleCharacteristicsNode->CreateChildNode(TEXT("fielddominance"))->SetContent(FString(TEXT("lower")));
return true;
}
bool FFCPXMLExportVisitor::ConstructAudioSampleCharacteristicsNode(TSharedRef<FFCPXMLNode> InParentNode, int InDepth, int InSampleRate)
{
TSharedRef<FFCPXMLNode> SampleCharacteristicsNode = InParentNode->CreateChildNode(TEXT("samplecharacteristics"));
SampleCharacteristicsNode->CreateChildNode(TEXT("depth"))->SetContent(InDepth);
SampleCharacteristicsNode->CreateChildNode(TEXT("samplerate"))->SetContent(InSampleRate);
return true;
}
bool FFCPXMLExportVisitor::ConstructRateNode(TSharedRef<FFCPXMLNode> InParentNode)
{
TSharedRef<FFCPXMLNode> RateNode = InParentNode->CreateChildNode(TEXT("rate"));
TSharedRef<FFCPXMLNode> TimebaseNode = RateNode->CreateChildNode(TEXT("timebase"));
TimebaseNode->SetContent(static_cast<int32>(ExportData->GetNearestWholeFrameRate()));
TSharedRef<FFCPXMLNode> NTSCNode = RateNode->CreateChildNode(TEXT("ntsc"));
NTSCNode->SetContent(ExportData->GetFrameRateIsNTSC());
return true;
}
bool FFCPXMLExportVisitor::ConstructTimecodeNode(TSharedRef<FFCPXMLNode> InParentNode)
{
TSharedRef<FFCPXMLNode> TimecodeNode = InParentNode->CreateChildNode(TEXT("timecode"));
if (!ConstructRateNode(TimecodeNode))
{
return false;
}
TimecodeNode->CreateChildNode(TEXT("string"))->SetContent(FString(TEXT("00:00:00:00")));
TimecodeNode->CreateChildNode(TEXT("frame"))->SetContent(0);
return true;
}
/** Get duration, in and out frames for a given video shot section */
bool FFCPXMLExportVisitor::GetCinematicSectionFrames(const TSharedPtr<FMovieSceneExportCinematicSectionData> InCinematicSectionData, int32& OutDuration, int32& OutStartFrame, int32&OutEndFrame, int32& OutInFrame, int32& OutOutFrame)
{
if (!InCinematicSectionData.IsValid() || !ExportData->MovieSceneData.IsValid())
{
return false;
}
int32 HandleFrames = ExportData->GetHandleFrames();
OutStartFrame = InCinematicSectionData->StartFrame.Value;
//
// The cinematic section's end frame is exclusive, FCPXML is also exclusive.
// For example, a 50 frame duration movie that starts at frame 0 should have an end frame value of 50.
// See corresponding note in FCPXMLMetadataExport.cpp ConstructVideoClipItemNode and ConstructMasterClipNode which
// constructs the end frame from the actual outputted frames.
//
OutEndFrame = InCinematicSectionData->EndFrame.Value;
OutDuration = OutEndFrame - OutStartFrame;
OutInFrame = HandleFrames;
OutOutFrame = HandleFrames + OutDuration;
return true;
}
/** Get duration, in and out frames for a given audio shot section */
bool FFCPXMLExportVisitor::GetAudioSectionFrames(const TSharedPtr<FMovieSceneExportAudioSectionData> InAudioSectionData, int32& OutDuration, int32& OutStartFrame, int32&OutEndFrame, int32& OutInFrame, int32& OutOutFrame)
{
if (!InAudioSectionData.IsValid() || !ExportData->MovieSceneData.IsValid())
{
return false;
}
OutStartFrame = InAudioSectionData->StartFrame.Value;
OutEndFrame = InAudioSectionData->EndFrame.Value;
OutDuration = OutEndFrame - OutStartFrame;
OutInFrame = 0;
OutOutFrame = OutDuration;
return true;
}
bool FFCPXMLExportVisitor::HasMasterClipIdName(const TSharedPtr<FMovieSceneExportSectionData> InSection, FString& OutName, bool& bOutMasterClipExists)
{
if (!InSection.IsValid())
{
return false;
}
FString Key;
if (!ComposeFileKey(InSection, Key))
{
return false;
}
if (MasterClipIdMap.Num() > 0)
{
uint32 *FoundId = MasterClipIdMap.Find(Key);
if (FoundId != nullptr)
{
OutName = FString::Printf(TEXT("masterclip-%d"), *FoundId);
bOutMasterClipExists = true;
return true;
}
}
bOutMasterClipExists = false;
return true;
}
bool FFCPXMLExportVisitor::GetMasterClipIdName(const TSharedPtr<FMovieSceneExportSectionData> InSection, FString& OutName)
{
if (!InSection.IsValid())
{
return false;
}
bool bMasterClipExists = false;
if (!HasMasterClipIdName(InSection, OutName, bMasterClipExists))
{
return false;
}
FString Key;
if (!ComposeFileKey(InSection, Key))
{
return false;
}
if (!bMasterClipExists)
{
++MasterClipId;
MasterClipIdMap.Add(Key, MasterClipId);
OutName = FString::Printf(TEXT("masterclip-%d"), MasterClipId);
}
return true;
}
bool FFCPXMLExportVisitor::GetFileIdName(const TSharedPtr<FMovieSceneExportSectionData> InSection, FString& OutFileIdName, bool& OutFileExists)
{
if (!InSection.IsValid())
{
return false;
}
FString Key;
if (!ComposeFileKey(InSection, Key))
{
return false;
}
if (FileIdMap.Num() > 0)
{
uint32 *FoundFileId = FileIdMap.Find(Key);
if (FoundFileId != nullptr)
{
OutFileIdName = FString::Printf(TEXT("file-%d"), *FoundFileId);
OutFileExists = true;
return true;
}
}
++FileId;
FileIdMap.Add(Key, FileId);
OutFileIdName = FString::Printf(TEXT("file-%d"), FileId);
OutFileExists = false;
return true;
}
void FFCPXMLExportVisitor::GetNextClipItemIdName(FString& OutName)
{
++ClipItemId;
OutName = FString::Printf(TEXT("clipitem-%d"), ClipItemId);
}
/** Compose a unique key string for audio sections based on channel */
bool FFCPXMLExportVisitor::ComposeFileKey(const TSharedPtr<FMovieSceneExportSectionData> InSection, FString& OutName)
{
if (!InSection.IsValid())
{
return false;
}
OutName = InSection->MovieSceneSection->GetPathName();
return true;
}
bool FFCPXMLExportVisitor::CreateCinematicSectionMetadata(const UMovieSceneCinematicShotSection* InSection, FString& OutMetadata) const
{
if (InSection == nullptr)
{
return false;
}
OutMetadata = TEXT("[UEShotSection=") + InSection->GetPathName() + TEXT("]");
// Store the start offset and the handle frames for round-tripping to compute the new start offset
int32 HandleFrames = ExportData->GetHandleFrames();
FFrameRate TickResolution = InSection->GetTypedOuter<UMovieScene>()->GetTickResolution();
int32 StartFrameOffset = ConvertFrameTime(InSection->Parameters.StartFrameOffset, TickResolution, ExportData->GetFrameRate()).CeilToFrame().Value;
OutMetadata += TEXT("[UEShotStartOffset=") + FString::FromInt(StartFrameOffset) + TEXT("]");
OutMetadata += TEXT("[UEShotHandleFrames=") + FString::FromInt(HandleFrames) + TEXT("]");
return true;
}
/** Get metadata section name from sequencer shot name - format is "[UESoundWave=soundwaveobjectname][UESoundSectionTopLevel=toplevelobjectname][UESoundSection=sectionobjectname]", whitespace ok. */
bool FFCPXMLExportVisitor::CreateSoundWaveMetadata(const USoundWave* InSoundWave, const TArray<const UMovieSceneAudioSection*> InAudioSections, FString& OutMetadata) const
{
if (InSoundWave == nullptr)
{
return false;
}
TArray<FString> SectionsAdded;
bool bTopLevelAdded = false;
OutMetadata = TEXT("[UESoundWave=") + InSoundWave->GetPathName() + TEXT("]");
for (const UMovieSceneAudioSection* AudioSection : InAudioSections)
{
if (!bTopLevelAdded)
{
OutMetadata += TEXT("[UEAudioSectionTopLevel=") + FFCPXMLExportVisitor::GetAudioSectionTopLevelName(AudioSection) + TEXT("]");
bTopLevelAdded = true;
}
// skip duplicate section names
FString SectionName = FFCPXMLExportVisitor::GetAudioSectionName(AudioSection);
if (SectionsAdded.Num() == 0 || !SectionsAdded.Contains(SectionName))
{
OutMetadata += TEXT("[UEAudioSection=") + SectionName + TEXT("]");
SectionsAdded.Add(SectionName);
}
}
return true;
}
FString FFCPXMLExportVisitor::GetAudioSectionTopLevelName(const UMovieSceneAudioSection* InAudioSection)
{
return InAudioSection->GetOutermost()->GetName();
}
FString FFCPXMLExportVisitor::GetAudioSectionName(const UMovieSceneSection* InAudioSection)
{
return InAudioSection->GetFullGroupName(false);
}
#undef LOCTEXT_NAMESPACE