From c0a718501a10712bbe32bad263fdbb93da388bff Mon Sep 17 00:00:00 2001 From: Sch <3516520171@qq.com> Date: Tue, 29 Aug 2023 11:23:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=A7=E9=87=8F=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Config/DefaultGame.ini | 7 + Resources/EngineLogo.ico | Bin 0 -> 137750 bytes .../Cut5/Interface/CutMainWidgetInterface.h | 1 + Source/Cut5/Interface/SoundInterface.cpp | 82 +++- Source/Cut5/Interface/SoundInterface.h | 16 +- Source/Cut5/Interface/VideoInterface.cpp | 177 +++++++++ Source/Cut5/Interface/VideoInterface.h | 23 ++ Source/Cut5/MainHUD.cpp | 9 +- Source/Cut5/MainHUD.h | 3 + Source/Cut5/Utils/FFMPEGUtils.cpp | 340 +++++++++------- Source/Cut5/Utils/Utils.cpp | 171 +++++++- Source/Cut5/Utils/Utils.h | 60 ++- .../Widgets/Commands/TimelineClipCommands.cpp | 2 + .../Widgets/Commands/TimelineClipCommands.h | 2 + Source/Cut5/Widgets/DefineGlobal.h | 18 +- .../DragDropOperator/DragDropOperator.cpp | 23 +- .../Cut5/Widgets/MicroWidgets/SColorPanel.cpp | 2 +- Source/Cut5/Widgets/SCustomInputPanel.cpp | 2 +- Source/Cut5/Widgets/SCutMainWindow.cpp | 219 +++++++++-- Source/Cut5/Widgets/SCutMainWindow.h | 16 +- Source/Cut5/Widgets/SCutTimeline.cpp | 102 +++-- Source/Cut5/Widgets/SCutTimeline.h | 49 +++ Source/Cut5/Widgets/STimelineClip.cpp | 368 ++++++------------ Source/Cut5/Widgets/STimelineClip.h | 7 +- Source/Cut5/Widgets/STimelineProperty.cpp | 1 + .../Cut5/Widgets/STimelinePropertyPanel.cpp | 16 + Source/Cut5/Widgets/STrackBody.cpp | 52 ++- Source/Cut5/Widgets/STrackBody.h | 9 +- .../Widgets/StatePanel/SLightArrayPanel.cpp | 6 +- 29 files changed, 1266 insertions(+), 517 deletions(-) create mode 100644 Resources/EngineLogo.ico create mode 100644 Source/Cut5/Interface/VideoInterface.cpp create mode 100644 Source/Cut5/Interface/VideoInterface.h diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index d325281..7728b9d 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -1,3 +1,10 @@ [/Script/EngineSettings.GeneralProjectSettings] ProjectID=DC8863584B4EB976B538D48259B614C0 +ProjectDisplayedTitle=NSLOCTEXT("[/Script/EngineSettings]", "AAF6DC1F478F1461B74DFBA59A7881EF", "编辑器") +ProjectDebugTitleInfo=NSLOCTEXT("[/Script/EngineSettings]", "BF92E12B419D88450FDB208106B9DE89", "\"") +bUseBorderlessWindow=True +bAllowMinimize=False +bAllowMaximize=False +bAllowClose=False + diff --git a/Resources/EngineLogo.ico b/Resources/EngineLogo.ico new file mode 100644 index 0000000000000000000000000000000000000000..8b7ea066c9ce2e51f1f8e1bfdc55f44e6d4e31a6 GIT binary patch literal 137750 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 #include "Cut5/Widgets/DefineGlobal.h" - +#include "Cut5/Utils/Utils.h" FCriticalSection Mutex; -FSoundThread::FSoundThread(int32 OutputChannel, int32 SampleRate) : FRunnable() +FSoundThread::FSoundThread(int32 OutputChannel, int32 SampleRate, int32 ByteNum, PaSampleFormat PaSoundType) : FRunnable() { Stream = nullptr; Pa_Initialize(); - Pa_OpenDefaultStream(&Stream, 0, OutputChannel, paFloat32, SampleRate, 0, nullptr, nullptr); + Pa_OpenDefaultStream(&Stream, 0, OutputChannel, PaSoundType, SampleRate, 10000, nullptr, nullptr); Pa_StartStream(Stream); this->OutputChannel = OutputChannel; this->SampleRate = SampleRate; + this->ByteNum = ByteNum; + this->PaSoundType = PaSoundType; } bool FSoundThread::Init() @@ -37,20 +39,23 @@ uint32 FSoundThread::Run() { while (!bStoped) { + if (SeekedFrame != 0) { - TArray> ResultData; - const int32 FrameLength = Audio.Num(); - for (int32 i = SeekedFrame * (SampleRate / 26) * 4 * 2; i < (SeekedFrame + 1) * (SampleRate / 26) * 4 * 2 && i < FrameLength; ++i) + const int32 Offset = SeekedFrame * (SampleRate / FGlobalData::GlobalFPS) * ByteNum * 2; + const int32 Size = SampleRate / FGlobalData::GlobalFPS; + if (Audio.Num() < Offset + Size) { - if (ResultData.Num() < i && i > 0) - { - ResultData.Add(Audio[i]); - } + SeekedFrame = 0; + continue; } - Pa_WriteStream(Stream, ResultData.GetData(), ResultData.Num() / 4 / 2); + if (Pa_WriteStream(Stream, Audio.GetData() + Offset, Size) == paOutputUnderflowed) + { + GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Underflowed")); + }; SeekedFrame = 0; } + } return 0; } @@ -61,6 +66,23 @@ void FSoundThread::Stop() FRunnable::Stop(); } +int FSoundThread::PaStreamCallback(const void* input, void* output, unsigned long frameCount, + const PaStreamCallbackTimeInfo* timeInfo, PaStreamCallbackFlags statusFlags, void* userData) +{ + // FSoundThread* This = static_cast(userData); + // + // TArray> ResultData; + // const int32 FrameLength = This->Audio.Num(); + // for (int32 i = This->SeekedFrame * (This->SampleRate / FGlobalData::GlobalFPS) * This->ByteNum * 2; i < This->SeekedFrame * (This->SampleRate / FGlobalData::GlobalFPS) * This->ByteNum * 2 + frameCount && i < FrameLength; ++i) + // { + // if (ResultData.Num() < i && i > 0) + // { + // ResultData.Add(This->Audio[i]); + // } + // } + // output = ResultData.GetData(); + return paContinue; +} bool FSoundThread::CopyAudio(const uint8* Data, int32 Size, ESoundSolveType SolveType) @@ -74,28 +96,28 @@ bool FSoundThread::CopyAudio(const uint8* Data, int32 Size, ESoundSolveType Solv } case ESoundSolveType::OnlyLeft: { - for (int32 i = 0; i < Size / 4; i++) + for (int32 i = 0; i < Size / ByteNum; i++) { if (i % 2 == 1) { - Audio[i * 4] = 0; - Audio[i * 4 + 1] = 0; - Audio[i * 4 + 2] = 0; - Audio[i * 4 + 3] = 0; + for (int32 j = 0; j < ByteNum; j++) + { + Audio[i * ByteNum + j] = 0; + } } } break; } case ESoundSolveType::OnlyRight: { - for (int32 i = 0; i < Size / 4; i++) + for (int32 i = 0; i < Size / ByteNum; i++) { if (i % 2 == 0) { - Audio[i * 4] = 0; - Audio[i * 4 + 1] = 0; - Audio[i * 4 + 2] = 0; - Audio[i * 4 + 3] = 0; + for (int32 j = 0; j < ByteNum; j++) + { + Audio[i * ByteNum + j] = 0; + } } } break; @@ -112,5 +134,23 @@ bool FSoundThread::CopyAudio(const uint8* Data, int32 Size, ESoundSolveType Solv bool FSoundThread::SeekFrame(int32 SeekFrameLoc) { SeekedFrame = SeekFrameLoc; + + // for (int32 i = 0; i < 200; i++) + // { + // const int32 Offset = i * (SampleRate / FGlobalData::GlobalFPS) * ByteNum * 2; + // const int32 Size = SampleRate / FGlobalData::GlobalFPS; + // Pa_WriteStream(Stream, Audio.GetData() + Offset, Size); + // } + return true; } + +void FSoundThread::StartPlay() +{ + bIsPlaying = true; +} + +void FSoundThread::StopPlay() +{ + bIsPlaying = false; +} diff --git a/Source/Cut5/Interface/SoundInterface.h b/Source/Cut5/Interface/SoundInterface.h index be5671a..889d269 100644 --- a/Source/Cut5/Interface/SoundInterface.h +++ b/Source/Cut5/Interface/SoundInterface.h @@ -9,16 +9,22 @@ extern "C"{ class FSoundThread : public FRunnable { public: - FSoundThread(int32 OutputChannel, int32 SampleRate); + FSoundThread(int32 OutputChannel, int32 SampleRate, int32 ByteNum, PaSampleFormat PaSoundType); virtual bool Init(); virtual void Exit() override; virtual uint32 Run() override; virtual void Stop() override; - + static int PaStreamCallback( + const void *input, void *output, + unsigned long frameCount, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData ); PaStream* Stream; - int32 OutputChannel; int32 SampleRate; + int32 ByteNum; + uint64 PaSoundType; bool bStoped = false; @@ -26,4 +32,8 @@ public: bool SeekFrame(int32 SeekFrameLoc); int32 SeekedFrame = 0; TArray Audio; + + void StartPlay(); + void StopPlay(); + bool bIsPlaying = false; }; diff --git a/Source/Cut5/Interface/VideoInterface.cpp b/Source/Cut5/Interface/VideoInterface.cpp new file mode 100644 index 0000000..57ef391 --- /dev/null +++ b/Source/Cut5/Interface/VideoInterface.cpp @@ -0,0 +1,177 @@ +#include "VideoInterface.h" + +#include "CutMainWidgetInterface.h" +#include "Cut5/Utils/FFMPEGUtils.h" + +bool FVideoThread::Init() +{ + FFFMPEGUtils::LoadContextPure(ClipData.MoviePath, &NewPropertyData); + if (NewPropertyData.VideoCodecContext == nullptr) + { + return false; + } + if (NewPropertyData.Context == nullptr) + { + return false; + } + + return true; +} + +uint32 FVideoThread::Run() +{ + while (!IsStop) + { + if (CurrentSeekingFrame != -1) + { + int32 VideoFPS = NewPropertyData.VideoCodecContext->framerate.num; + if (VideoFPS < FGlobalData::GlobalFPS) + { + const double Interval = FGlobalData::GlobalFPS / VideoFPS; + CurrentSeekingFrame = static_cast(CurrentSeekingFrame) / Interval; + } + else + { + VideoFPS = FGlobalData::GlobalFPS; + } + + const int64 Timestamp = av_rescale_q(static_cast(CurrentSeekingFrame) / static_cast(VideoFPS) * AV_TIME_BASE + , AVRational{1, AV_TIME_BASE} + , NewPropertyData.Context->streams[NewPropertyData.VideoStream]->time_base); + + + if (CurrentSeekingFrame - LastSeekFrame > 1 || CurrentSeekingFrame - LastSeekFrame < 0 || LastSeekFrame == -1) + { + av_seek_frame(NewPropertyData.Context, NewPropertyData.VideoStream, Timestamp, AVSEEK_FLAG_BACKWARD); + } + + + + AVPacket* Packet = av_packet_alloc(); + AVFrame* AllocatedFrame = av_frame_alloc(); + + av_init_packet(Packet); + avcodec_receive_frame(NewPropertyData.VideoCodecContext, AllocatedFrame); + int32 Times = 0; + + bool bNeedReadFrame = true; + if (CurrentSeekingFrame == LastSeekFrame && LastSeekFrame != -1) + { + if (LastFrame != nullptr) + { + AllocatedFrame = LastFrame; + bNeedReadFrame = false; + } + + } + + bool IsFailed = false; + if (bNeedReadFrame) + { + while (av_read_frame(NewPropertyData.Context, Packet) >= 0) + { + if (Packet->stream_index == NewPropertyData.VideoStream) + { + int32 Response = avcodec_send_packet(NewPropertyData.VideoCodecContext, Packet); + if (Response < 0) + { + UE_LOG(LogTemp, Error, TEXT("Error while sending a packet to the decoder: %s"), *FString(FString::FromInt(Response))); + IsFailed = true; + } + Response = avcodec_receive_frame(NewPropertyData.VideoCodecContext, AllocatedFrame); + if (Response == AVERROR(EAGAIN) || Response == AVERROR_EOF) + { + } + else if (Response < 0) + { + UE_LOG(LogTemp, Error, TEXT("Error while receiving a frame from the decoder: %s"), *FString(FString::FromInt(Response))); + IsFailed = true; + } + if (AllocatedFrame->best_effort_timestamp >= Timestamp) + { + LastFrame = AllocatedFrame; + av_packet_unref(Packet); + break; + } + } + } + } + av_packet_unref(Packet); + + if (IsFailed == true) + { + CurrentSeekingFrame = -1; + continue;; + } + + + const AVCodecContext* VideoCodecContext = NewPropertyData.VideoCodecContext; + struct SwsContext* SwsCtx = sws_getContext( + AllocatedFrame->width, AllocatedFrame->height, VideoCodecContext->pix_fmt, + AllocatedFrame->width, AllocatedFrame->height, AV_PIX_FMT_BGRA, + SWS_BILINEAR, nullptr, nullptr, nullptr + ); + if (!SwsCtx) + { + UE_LOG(LogTemp, Error, TEXT("Error creating swsContext")); + CurrentSeekingFrame = -1; + continue;; + } + + + uint8* RawData = new uint8[AllocatedFrame->width * AllocatedFrame->height * 4]; + uint8* Dest[4] = {RawData, nullptr, nullptr, nullptr}; + const int32 DestLineSize[4] = {AllocatedFrame->width * 4, 0, 0, 0}; + sws_scale(SwsCtx, AllocatedFrame->data, AllocatedFrame->linesize, 0, AllocatedFrame->height, Dest, DestLineSize); + sws_freeContext(SwsCtx); + + int32 DataSize = AllocatedFrame->width * AllocatedFrame->height * 4; + if (AllocatedFrame->data[0] == nullptr) + { + CurrentSeekingFrame = -1; + delete RawData; + continue;; + } + + + int32 X = AllocatedFrame->width; + int32 Y = AllocatedFrame->height; + AsyncTask(ENamedThreads::GameThread, [this, X, Y, RawData]() + { + MainInterface->OnUpdateVideo(FGuid(), X, Y, RawData); + + + }); + LastFrame = av_frame_alloc(); + // av_frame_copy(LastFrame, AllocatedFrame); + LastFrame = av_frame_clone(AllocatedFrame); + av_frame_free(&AllocatedFrame); + LastSeekFrame = CurrentSeekingFrame.load(); + CurrentSeekingFrame = -1; + } + } + return 0; +} + +void FVideoThread::Stop() +{ + IsStop = true; +} + +void FVideoThread::Exit() +{ + FRunnable::Exit(); +} + +FVideoThread::FVideoThread(TFunction GameThreadCallbackFunction, + const FClipData& InitClipData, ICutMainWidgetInterface* InMainInterface) +{ + BindFunction = GameThreadCallbackFunction; + ClipData = InitClipData; + MainInterface = InMainInterface; +} + +void FVideoThread::SeekFrame(int32 Frame) +{ + CurrentSeekingFrame = Frame; +} diff --git a/Source/Cut5/Interface/VideoInterface.h b/Source/Cut5/Interface/VideoInterface.h new file mode 100644 index 0000000..060bd60 --- /dev/null +++ b/Source/Cut5/Interface/VideoInterface.h @@ -0,0 +1,23 @@ +#pragma once +#include "Cut5/Widgets/DefineGlobal.h" + +class FVideoThread final: public FRunnable +{ + virtual bool Init() override; + + virtual void Exit() override; +public: + virtual void Stop() override; + FVideoThread(TFunction GameThreadCallbackFunction, const FClipData& InitClipData, ICutMainWidgetInterface* InMainInterface); + + ICutMainWidgetInterface* MainInterface = nullptr; + void SeekFrame(int32 Frame); + virtual uint32 Run() override; + TFunction BindFunction; + FClipData ClipData; + FTimelinePropertyData NewPropertyData; + std::atomic CurrentSeekingFrame = -1; + std::atomic LastSeekFrame = -1; + AVFrame* LastFrame = nullptr; + bool IsStop = false; +}; diff --git a/Source/Cut5/MainHUD.cpp b/Source/Cut5/MainHUD.cpp index d87f547..b38212d 100644 --- a/Source/Cut5/MainHUD.cpp +++ b/Source/Cut5/MainHUD.cpp @@ -13,7 +13,7 @@ AMainHUD::AMainHUD() { if (GEngine && GEngine->GameViewport) { - const TSharedPtr MainWindow = SNew(SCutMainWindow); + SAssignNew(MainWindow, SCutMainWindow); GEngine->GameViewport->AddViewportWidgetContent( MainWindow.ToSharedRef() ); @@ -28,7 +28,12 @@ AMainHUD::AMainHUD() UWidgetBlueprintLibrary::SetInputMode_UIOnlyEx(UGameplayStatics::GetPlayerController(GWorld, 0), nullptr, EMouseLockMode::DoNotLock); UGameplayStatics::GetPlayerController(GWorld, 0)->bShowMouseCursor = true; - + + GEngine->GameViewport->GetWindow()->SetWindowMode(EWindowMode::Windowed); + GEngine->GameViewport->GetWindow()->GetTitleBar()->UpdateBackgroundContent(nullptr); + + + UGameplayStatics::GetPlayerController(GWorld->GetWorld(), 0)->ConsoleCommand("t.setRes 1280x720w"); } diff --git a/Source/Cut5/MainHUD.h b/Source/Cut5/MainHUD.h index c63492a..f6f6346 100644 --- a/Source/Cut5/MainHUD.h +++ b/Source/Cut5/MainHUD.h @@ -4,6 +4,7 @@ #include "CoreMinimal.h" #include "GameFramework/HUD.h" +#include "Widgets/SCutMainWindow.h" #include "MainHUD.generated.h" /** @@ -16,4 +17,6 @@ class CUT5_API AMainHUD : public AHUD AMainHUD(); ~AMainHUD(); + + TSharedPtr MainWindow; }; diff --git a/Source/Cut5/Utils/FFMPEGUtils.cpp b/Source/Cut5/Utils/FFMPEGUtils.cpp index 23899a3..3ac27c4 100644 --- a/Source/Cut5/Utils/FFMPEGUtils.cpp +++ b/Source/Cut5/Utils/FFMPEGUtils.cpp @@ -107,18 +107,26 @@ FString FFFMPEGUtils::LoadMedia(const FString& Path, FTimelinePropertyData* Prop if (AudioStream != -1) { + AVCodecContext* AudioCodecContext = avcodec_alloc_context3(nullptr); avcodec_parameters_to_context(AudioCodecContext, FormatContext->streams[AudioStream]->codecpar); AVCodec* AudioCodec = avcodec_find_decoder(AudioCodecContext->codec_id); + + + if (avcodec_open2(AudioCodecContext, AudioCodec, nullptr) < 0) { return TEXT("Failed"); } + + TArray DataResult; AVPacket Packet = *av_packet_alloc(); AVFrame* Frame = av_frame_alloc(); - while (1) + + const AVSampleFormat SampleFormat = *AudioCodecContext->codec->sample_fmts; + while (true) { if (av_read_frame(FormatContext, &Packet) < 0) { @@ -127,23 +135,31 @@ FString FFFMPEGUtils::LoadMedia(const FString& Path, FTimelinePropertyData* Prop break; } } - avcodec_send_packet(AudioCodecContext, &Packet); - if (avcodec_receive_frame(AudioCodecContext, Frame) >= 0) + if (AudioCodec->id < 0x10800 && AudioCodec->id >= 0x10000) { - const uint8* Result = FUtils::ConvertTwoChannelSound2PortAudioSound(Frame->data[0], Frame->data[1], Frame->nb_samples); - if (Result != nullptr) - { - DataResult.Append(Result, Frame->nb_samples * 4 * 2); - // Pa_WriteStream(Stream, Result, Frame->nb_samples); - } - delete[] Result; + DataResult.Append(Packet.data, Packet.size); } + else + { + avcodec_send_packet(AudioCodecContext, &Packet); + if (avcodec_receive_frame(AudioCodecContext, Frame) >= 0) + { + const uint8* Result = FUtils::ConvertTwoChannelSound2PortAudioSound(Frame->data, Frame->channels, Frame->nb_samples, FUtils::GetFormatSampleBytesNum(SampleFormat)); + if (Result != nullptr) + { + DataResult.Append(Result, Frame->nb_samples * FUtils::GetFormatSampleBytesNum(SampleFormat) * Frame->channels); + } + delete[] Result; + } + } + } PropertyData->AudioData = DataResult; PropertyData->AudioCodecContext = AudioCodecContext; PropertyData->AudioCodec = AudioCodec; PropertyData->AudioSample = AudioCodecContext->sample_rate; PropertyData->AudioChannels = AudioCodecContext->channels; + PropertyData->SampleFormat = SampleFormat; } @@ -419,133 +435,183 @@ TArray FFFMPEGUtils::GetMovieBrush(FClipData* ClipData, bool Regene TArray FFFMPEGUtils::GetAudioBrush(FClipData* ClipData) { - const float TimeLength = (ClipData->ClipEndFrame - ClipData->ClipStartFrame) * FGlobalData::DefaultTimeTickSpace; - - const int32 PicLength = TimeLength / 8.0; - - if (ClipData->ResourcePropertyDataPtr) - { - if (ClipData->ResourcePropertyDataPtr->AudioData.Num() == 0) - { - return {}; - } - if (ClipData->ResourcePropertyDataPtr->AudioStream == -1) - { - return {}; - } - if (ClipData->ResourcePropertyDataPtr->VideoStream != -1) - { - return {}; - } - } - else - { - return {}; - } - - int32 DownSampleSpace = 128; - TArray DownSampledData; - DownSampledData.SetNumZeroed(ClipData->ResourcePropertyDataPtr->AudioData.Num() / 4 / DownSampleSpace); - for (int32 i = 0; i < ClipData->ResourcePropertyDataPtr->AudioData.Num() / 4; i++) - { - float NewFloat = *reinterpret_cast(ClipData->ResourcePropertyDataPtr->AudioData.GetData() + (i * 4)); - if (i % DownSampleSpace == 0) - { - DownSampledData[i / DownSampleSpace] = NewFloat; - } - } - - - const float FFTSize = DownSampledData.Num(); - kiss_fft_cfg Cfg = kiss_fft_alloc(FFTSize, 0, nullptr, nullptr); - - TArray KissIn, KissOut; - KissIn.SetNumZeroed(FFTSize); - KissOut.SetNumZeroed(FFTSize); - - for (int32 i = 0; i < DownSampledData.Num(); i++) - { - float NewFloat = DownSampledData[i]; - KissIn[i].r = NewFloat; - KissIn[i].i = 0; - } - - kiss_fft(Cfg, KissIn.GetData(), KissOut.GetData()); - - TArray Spectrum; - Spectrum.SetNumZeroed(FFTSize); - for (int32 i = 0; i < DownSampledData.Num(); i++) - { - Spectrum[i] = sqrt(KissOut[i].r * KissOut[i].r + KissOut[i].i * KissOut[i].i); - } - - float MaxValue = 0; - float MinValue = 0; - for (int32 i = 0; i < Spectrum.Num(); i++) - { - float NewFloat = ClipData->ResourcePropertyDataPtr->AudioData[i]; - - if (NewFloat >= MaxValue) - { - MaxValue = NewFloat; - } - if (NewFloat <= MinValue) - { - MinValue = NewFloat; - } - } - - - auto Normalize = [MaxValue, MinValue](float CurrentValue) - { - return FMath::GetMappedRangeValueClamped(FVector2D(MinValue, MaxValue), FVector2D(FGlobalData::DefaultTrackHeight, 0), CurrentValue); - }; - - ClipData->AudioBrushLength.Empty(); - ClipData->AudioBrushes.Empty(); - int32 Index = 0; - int32 TotalLength = TimeLength; - float LastPoint = 0.0; - for (int32 i = 0; i < TotalLength / PicLength; i++) - { - FLinearColor RenderColor(i * (360.0 / 8.0), 1.0, 1.0); - UTextureRenderTarget2D* TextureRenderTarget2D = NewObject(); - TextureRenderTarget2D->InitCustomFormat(PicLength, int32(FGlobalData::DefaultTrackHeight), PF_B8G8R8A8, false); - TextureRenderTarget2D->UpdateResourceImmediate(); - UKismetRenderingLibrary::ClearRenderTarget2D(GWorld->GetWorld(), TextureRenderTarget2D, FLinearColor(0, 0, 0, 0)); - UCanvas* Canvas; - FVector2D Size; - FDrawToRenderTargetContext RenderTargetContext; - UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(GWorld->GetWorld(), TextureRenderTarget2D, Canvas, Size, RenderTargetContext); - - int32 StartIndex = (Index * PicLength) / 16; - - for (int32 j = 0; j < PicLength; j++) - { - float CurrentData = Spectrum[StartIndex]; - float NormalizedData = Normalize(CurrentData); - - Canvas->K2_DrawLine(FVector2D(j - 1, LastPoint), FVector2D(j, NormalizedData), 1, RenderColor.HSVToLinearRGB()); - LastPoint = NormalizedData; - StartIndex += 1; - } - - - - - - UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(GWorld->GetWorld(), RenderTargetContext); - - - FBufferArchive Buffer; - FImageUtils::ExportRenderTarget2DAsPNG(TextureRenderTarget2D, Buffer); - FFileHelper::SaveArrayToFile(Buffer, *FPaths::Combine(FUtils::GetProjectTempPath(), ClipData->ClipGuid.ToString(), FString::FromInt(Index) + ".png")); - TextureRenderTarget2D->MarkAsGarbage(); - FSlateDynamicImageBrush SlateBrush = FSlateDynamicImageBrush(*FPaths::Combine(FUtils::GetProjectTempPath(), ClipData->ClipGuid.ToString(), FString::FromInt(Index) + ".png"), FVector2D(PicLength, FGlobalData::DefaultTrackHeight)); - ClipData->AudioBrushLength.Add(PicLength); - ClipData->AudioBrushes.Add(SlateBrush); - Index++; - } + // const float TimeLength = (ClipData->ClipEndFrame - ClipData->ClipStartFrame) * FGlobalData::DefaultTimeTickSpace; + // + // const int32 PicLength = TimeLength / 8.0; + // + // if (ClipData->ResourcePropertyDataPtr) + // { + // if (ClipData->ResourcePropertyDataPtr->AudioData.Num() == 0) + // { + // return {}; + // } + // if (ClipData->ResourcePropertyDataPtr->AudioStream == -1) + // { + // return {}; + // } + // if (ClipData->ResourcePropertyDataPtr->VideoStream != -1) + // { + // return {}; + // } + // } + // else + // { + // return {}; + // } + // + // + // int32 DownSampleSpace = 128; + // TArray DownSampledData; + // DownSampledData.SetNumZeroed(ClipData->ResourcePropertyDataPtr->AudioData.Num() / FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat) / DownSampleSpace); + // for (int32 i = 0; i < ClipData->ResourcePropertyDataPtr->AudioData.Num() / FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat) / DownSampleSpace; i++) + // { + // switch (FUtils::GetFormatType(ClipData->ResourcePropertyDataPtr->SampleFormat)) + // { + // case 0: + // { + // uint8 NewValue = *reinterpret_cast(ClipData->ResourcePropertyDataPtr->AudioData.GetData() + (i * FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat))); + // if (i % DownSampleSpace == 0) + // { + // DownSampledData[i / DownSampleSpace] = NewValue; + // } + // break; + // } + // + // case 1: + // { + // int16 NewValue = *reinterpret_cast(ClipData->ResourcePropertyDataPtr->AudioData.GetData() + (i * FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat))); + // if (i % DownSampleSpace == 0) + // { + // DownSampledData[i / DownSampleSpace] = NewValue; + // } + // break; + // } + // case 2: + // { + // int32 NewValue = *reinterpret_cast(ClipData->ResourcePropertyDataPtr->AudioData.GetData() + (i * FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat))); + // if (i % DownSampleSpace == 0) + // { + // DownSampledData[i / DownSampleSpace] = NewValue; + // } + // break; + // } + // case 3: + // { + // float NewValue = *reinterpret_cast(ClipData->ResourcePropertyDataPtr->AudioData.GetData() + (i * FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat))); + // if (i % DownSampleSpace == 0) + // { + // DownSampledData[i / DownSampleSpace] = NewValue; + // } + // break; + // } + // case 4: + // { + // double NewValue = *reinterpret_cast(ClipData->ResourcePropertyDataPtr->AudioData.GetData() + (i * FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat))); + // if (i % DownSampleSpace == 0) + // { + // DownSampledData[i / DownSampleSpace] = NewValue; + // } + // break; + // } + // default: + // break; + // } + // ; + // + // + // } + // + // + // const float FFTSize = DownSampledData.Num(); + // kiss_fft_cfg Cfg = kiss_fft_alloc(FFTSize, 0, nullptr, nullptr); + // + // TArray KissIn, KissOut; + // KissIn.SetNumZeroed(FFTSize); + // KissOut.SetNumZeroed(FFTSize); + // + // for (int32 i = 0; i < DownSampledData.Num(); i++) + // { + // float NewFloat = DownSampledData[i]; + // KissIn[i].r = NewFloat; + // KissIn[i].i = 0; + // } + // + // kiss_fft(Cfg, KissIn.GetData(), KissOut.GetData()); + // + // TArray Spectrum; + // Spectrum.SetNumZeroed(FFTSize); + // for (int32 i = 0; i < DownSampledData.Num(); i++) + // { + // Spectrum[i] = sqrt(KissOut[i].r * KissOut[i].r + KissOut[i].i * KissOut[i].i); + // } + // + // float MaxValue = 0; + // float MinValue = 0; + // for (int32 i = 0; i < Spectrum.Num(); i++) + // { + // float NewFloat = ClipData->ResourcePropertyDataPtr->AudioData[i]; + // + // if (NewFloat >= MaxValue) + // { + // MaxValue = NewFloat; + // } + // if (NewFloat <= MinValue) + // { + // MinValue = NewFloat; + // } + // } + // + // + // auto Normalize = [MaxValue, MinValue](float CurrentValue) + // { + // return FMath::GetMappedRangeValueClamped(FVector2D(MinValue, MaxValue), FVector2D(FGlobalData::DefaultTrackHeight, 0), CurrentValue); + // }; + // + // ClipData->AudioBrushLength.Empty(); + // ClipData->AudioBrushes.Empty(); + // int32 Index = 0; + // int32 TotalLength = TimeLength; + // float LastPoint = 0.0; + // for (int32 i = 0; i < TotalLength / PicLength; i++) + // { + // FLinearColor RenderColor(i * (360.0 / 8.0), 1.0, 1.0); + // UTextureRenderTarget2D* TextureRenderTarget2D = NewObject(); + // TextureRenderTarget2D->InitCustomFormat(PicLength, int32(FGlobalData::DefaultTrackHeight), PF_B8G8R8A8, false); + // TextureRenderTarget2D->UpdateResourceImmediate(); + // UKismetRenderingLibrary::ClearRenderTarget2D(GWorld->GetWorld(), TextureRenderTarget2D, FLinearColor(0, 0, 0, 0)); + // UCanvas* Canvas; + // FVector2D Size; + // FDrawToRenderTargetContext RenderTargetContext; + // UKismetRenderingLibrary::BeginDrawCanvasToRenderTarget(GWorld->GetWorld(), TextureRenderTarget2D, Canvas, Size, RenderTargetContext); + // + // int32 StartIndex = (Index * PicLength) / 16; + // + // for (int32 j = 0; j < PicLength; j++) + // { + // float CurrentData = Spectrum[StartIndex]; + // float NormalizedData = Normalize(CurrentData); + // + // Canvas->K2_DrawLine(FVector2D(j - 1, LastPoint), FVector2D(j, NormalizedData), 1, RenderColor.HSVToLinearRGB()); + // LastPoint = NormalizedData; + // StartIndex += 1; + // } + // + // + // + // + // + // UKismetRenderingLibrary::EndDrawCanvasToRenderTarget(GWorld->GetWorld(), RenderTargetContext); + // + // + // FBufferArchive Buffer; + // FImageUtils::ExportRenderTarget2DAsPNG(TextureRenderTarget2D, Buffer); + // FFileHelper::SaveArrayToFile(Buffer, *FPaths::Combine(FUtils::GetProjectTempPath(), ClipData->ClipGuid.ToString(), FString::FromInt(Index) + ".png")); + // TextureRenderTarget2D->MarkAsGarbage(); + // FSlateDynamicImageBrush SlateBrush = FSlateDynamicImageBrush(*FPaths::Combine(FUtils::GetProjectTempPath(), ClipData->ClipGuid.ToString(), FString::FromInt(Index) + ".png"), FVector2D(PicLength, FGlobalData::DefaultTrackHeight)); + // ClipData->AudioBrushLength.Add(PicLength); + // ClipData->AudioBrushes.Add(SlateBrush); + // Index++; + // } diff --git a/Source/Cut5/Utils/Utils.cpp b/Source/Cut5/Utils/Utils.cpp index 5bdba2d..88445b5 100644 --- a/Source/Cut5/Utils/Utils.cpp +++ b/Source/Cut5/Utils/Utils.cpp @@ -47,25 +47,28 @@ uint8* FUtils::ConvertFfMpegSound2PortAudioSound(uint8* inData[8], int32 Size) } -uint8* FUtils::ConvertTwoChannelSound2PortAudioSound(uint8* Channel1, uint8* Channel2, int32 Size) +uint8* FUtils::ConvertTwoChannelSound2PortAudioSound(uint8* Channel[], const int32 ChannelNum, const int32 Size, const int32 SampleBytes) { - void* Data = FMemory::Malloc(Size * 4 * 2); - FMemory::Memset(Data, 0, Size * 4 * 2); - if (Channel1 == nullptr) + if (Channel == nullptr) return nullptr; - if (Channel2 == nullptr) - return nullptr; - if (*Channel1 == '\0' || *Channel2 == '\0') + for (int32 i = 0; i < ChannelNum; i++) { - FMemory::Free(Data); - Data = nullptr; - return nullptr; + if (Channel[i] == nullptr) + return nullptr; + if (*Channel[i] == '\0') + return nullptr; } + + void* Data = FMemory::Malloc(Size * SampleBytes * ChannelNum); + FMemory::Memset(Data, 0, Size * SampleBytes * ChannelNum); + float* DataPtr = static_cast(Data); for (int32 i = 0; i < Size; i++) { - *(DataPtr + i * 2) = *(reinterpret_cast(Channel1) + i); - *(DataPtr + i * 2 + 1) = *(reinterpret_cast(Channel2) + i); + for (int32 j = 0; j < ChannelNum; j++) + { + *(DataPtr + i * ChannelNum + j) = *(reinterpret_cast(Channel[j]) + i); + } } return static_cast(Data); @@ -92,6 +95,150 @@ FSlateDynamicImageBrush* FUtils::GetBrushFromImage(const FString& ImageName, con return Brush; } +TArray FUtils::SingleColor2ColorArray(FColor Color) +{ + + int32 i = FGlobalData::LightArrayX * FGlobalData::LightArrayY; + TArray ColorArray; + while (i--) + { + ColorArray.Add(Color); + } + return ColorArray; +} + +int32 FUtils::GetFormatSampleBytesNum(const AVSampleFormat Format) +{ + if (Format == AV_SAMPLE_FMT_U8 || Format == AV_SAMPLE_FMT_U8P) + { + return 1; + } + else if (Format == AV_SAMPLE_FMT_S16 || Format == AV_SAMPLE_FMT_S16P) + { + return 2; + } + else if (Format == AV_SAMPLE_FMT_S32 || Format == AV_SAMPLE_FMT_S32P) + { + return 4; + } + else if (Format == AV_SAMPLE_FMT_FLT || Format == AV_SAMPLE_FMT_FLTP) + { + return 4; + } + else if (Format == AV_SAMPLE_FMT_DBL || Format == AV_SAMPLE_FMT_DBLP) + { + return 8; + } + else if (Format == AV_SAMPLE_FMT_U8P) + { + return 1; + } + else if (Format == AV_SAMPLE_FMT_S16P) + { + return 2; + } + else if (Format == AV_SAMPLE_FMT_S32P) + { + return 4; + } + else if (Format == AV_SAMPLE_FMT_FLTP) + { + return 4; + } + else if (Format == AV_SAMPLE_FMT_DBLP) + { + return 8; + } + else + { + return 0; + } +} + +int32 FUtils::GetFormatType(const AVSampleFormat Format) +{ + int32 Type = -1; + switch (Format) + { + case AV_SAMPLE_FMT_U8: + { + Type = 0; + } + break; + case AV_SAMPLE_FMT_S16: + { + Type = 1; + } + break; + case AV_SAMPLE_FMT_S32: + { + Type = 2; + } + break; + case AV_SAMPLE_FMT_FLT: + { + Type = 3; + } + break; + case AV_SAMPLE_FMT_DBL: + { + Type = 4; + } + break; + case AV_SAMPLE_FMT_U8P: + { + Type = 0; + } + break; + case AV_SAMPLE_FMT_S16P: + { + Type = 1; + } + break; + case AV_SAMPLE_FMT_S32P: + { + Type = 2; + } + break; + case AV_SAMPLE_FMT_FLTP: + { + Type = 3; + } + break; + case AV_SAMPLE_FMT_DBLP: + { + Type = 4; + } + break; + default: + { + Type = -1; + } + break; + } + return Type; +} + +FGuid FUtils::GetSoundThreadGuid(const FGuid& ClipGuid) +{ + FGuid NewGuid = ClipGuid; + NewGuid.A += 20; + NewGuid.B += 20; + NewGuid.C += 20; + NewGuid.D += 20; + return NewGuid; +} + +FGuid FUtils::GetVideoThreadGuid(const FGuid& ClipGuid) +{ + FGuid NewGuid = ClipGuid; + NewGuid.A += 40; + NewGuid.B += 40; + NewGuid.C += 40; + NewGuid.D += 40; + return NewGuid; +} + bool FUtils::DetectDragTypeCanDrop(const FClipData& DraggingType, const ETrackType& DropTrackType) { if (DropTrackType == ETrackType::LightArrayTrack || DropTrackType == ETrackType::LightBarTrack || DropTrackType == ETrackType::SpotLightTrack || DropTrackType == ETrackType::AtomSphereLightTrack) diff --git a/Source/Cut5/Utils/Utils.h b/Source/Cut5/Utils/Utils.h index d58386e..ac9fe79 100644 --- a/Source/Cut5/Utils/Utils.h +++ b/Source/Cut5/Utils/Utils.h @@ -9,13 +9,20 @@ public: // Size is nb_samples if it was float type static uint8* ConvertFfMpegSound2PortAudioSound(uint8* inData[8], int32 Size); - static uint8* ConvertTwoChannelSound2PortAudioSound(uint8* Channel1, uint8* Channel2, int32 Size); + static uint8* ConvertTwoChannelSound2PortAudioSound(uint8* Channel[], const int32 ChannelNum, const int32 Size, const int32 SampleBytes); static FString GetResourcesPath(FString ResourcesName, bool bFullPath = false); static FString GetProjectTempPath(); static FSlateDynamicImageBrush* GetBrushFromImage(const FString& ImageName, const FVector2D Size); static TArray BrushPtr; + static TArray SingleColor2ColorArray(FColor Color); + static int32 GetFormatSampleBytesNum(const AVSampleFormat Format); + static int32 GetFormatType(const AVSampleFormat Format); + template + static T* CastTypeByFormat(U* InValue, AVSampleFormat* Format); + static FGuid GetSoundThreadGuid(const FGuid& ClipGuid); + static FGuid GetVideoThreadGuid(const FGuid& ClipGuid); static bool DetectDragTypeCanDrop(const FClipData& DraggingType, const ETrackType& DropTrackType); /** @@ -82,6 +89,57 @@ public: static FString Color2Hex3(FColor Color); }; +template +T* FUtils::CastTypeByFormat(U* InValue, AVSampleFormat* Format) +{ + if (*Format == AV_SAMPLE_FMT_U8 || *Format == AV_SAMPLE_FMT_U8P) + { + return reinterpret_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_S16 || *Format == AV_SAMPLE_FMT_S16P) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_S32 || *Format == AV_SAMPLE_FMT_S32P) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_FLT || *Format == AV_SAMPLE_FMT_FLTP) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_DBL || *Format == AV_SAMPLE_FMT_DBLP) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_U8P) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_S16P) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_S32P) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_FLTP) + { + return static_cast(InValue); + } + else if (*Format == AV_SAMPLE_FMT_DBLP) + { + return static_cast(InValue); + } + else + { + return InValue; + } +} + + + class FSaveModifier { public: diff --git a/Source/Cut5/Widgets/Commands/TimelineClipCommands.cpp b/Source/Cut5/Widgets/Commands/TimelineClipCommands.cpp index 95b8b75..612474f 100644 --- a/Source/Cut5/Widgets/Commands/TimelineClipCommands.cpp +++ b/Source/Cut5/Widgets/Commands/TimelineClipCommands.cpp @@ -5,5 +5,7 @@ void FTimelineClipCommands::RegisterCommands() { UI_COMMAND(Remove, "移除", "Executes My TimelineClipCommands", EUserInterfaceActionType::Button, FInputChord()); UI_COMMAND(Break, "分割", "Executes My TimelineClipCommands", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(Fill2Start, "填充到开头", "Executes My TimelineClipCommands", EUserInterfaceActionType::Button, FInputChord()); + UI_COMMAND(Fill2End, "填充到结尾", "Executes My TimelineClipCommands", EUserInterfaceActionType::Button, FInputChord()); } #undef LOCTEXT_NAMESPACE \ No newline at end of file diff --git a/Source/Cut5/Widgets/Commands/TimelineClipCommands.h b/Source/Cut5/Widgets/Commands/TimelineClipCommands.h index 37f4eaa..276d322 100644 --- a/Source/Cut5/Widgets/Commands/TimelineClipCommands.h +++ b/Source/Cut5/Widgets/Commands/TimelineClipCommands.h @@ -19,4 +19,6 @@ public: TSharedPtr Remove; TSharedPtr Break; + TSharedPtr Fill2Start; + TSharedPtr Fill2End; }; \ No newline at end of file diff --git a/Source/Cut5/Widgets/DefineGlobal.h b/Source/Cut5/Widgets/DefineGlobal.h index bdccef4..2eac0f4 100644 --- a/Source/Cut5/Widgets/DefineGlobal.h +++ b/Source/Cut5/Widgets/DefineGlobal.h @@ -261,7 +261,7 @@ struct CUT5_API FPresetsCustomData TArray Colors = { FLinearColor(1, 1 , 1) }; int32 Times = 1; - float Angle; + float Angle = 0.0; float Time = 0.3; EPresetCustomType PresetCustomType = EPresetCustomType::None; @@ -375,8 +375,6 @@ struct CUT5_API FClipData { VideoStartFrame += CropFrame; } - - } else { @@ -402,6 +400,7 @@ struct CUT5_API FClipData // A Ptr to Input Resource; FTimelinePropertyData* ResourcePropertyDataPtr = nullptr; FGuid ResourcePropertyGuid; + AVSampleFormat SampleFormat; // Movies FString MoviePath; @@ -509,6 +508,14 @@ struct CUT5_API FTimelinePropertyData Type = InType; IconPath = InIconPath; } + + FTimelinePropertyData(bool InCanUse) + { + CanUse = InCanUse; + Name = ""; + Type = ETrackType::None; + IconPath = ""; + } bool bIsValid = true; FString Name = ""; ETrackType Type = ETrackType::VideoTrack; @@ -526,13 +533,14 @@ struct CUT5_API FTimelinePropertyData int32 AudioStream = -1; int32 AudioSample = 0; int32 AudioChannels = 2; + AVSampleFormat SampleFormat; // Base Data FString MoviePath = ""; int32 MovieFrameLength = 0; TArray AudioData; - + bool CanUse = true; bool bIsCustomPresetData = false; FPresetsCustomData PresetsCustomData = FPresetsCustomData(); @@ -994,7 +1002,7 @@ struct FProperties struct FEncodeVideoInfo { - FString EncodedVideoName = ".mp4"; + FString EncodedVideoName = ""; FString EncodedVideoTimeCode = "00:00:00:00"; int32 ClipStartFrame = 0; int32 ClipEndFrame = 0; diff --git a/Source/Cut5/Widgets/DragDropOperator/DragDropOperator.cpp b/Source/Cut5/Widgets/DragDropOperator/DragDropOperator.cpp index e80d159..632ed77 100644 --- a/Source/Cut5/Widgets/DragDropOperator/DragDropOperator.cpp +++ b/Source/Cut5/Widgets/DragDropOperator/DragDropOperator.cpp @@ -25,7 +25,7 @@ void DragDropOperator::UpdateClipProcess(ICutMainWidgetInterface* MainInterface, { if (!MainInterface) return; - + int32 MaxLength = 0; for (FSingleTrackGroupInstance& SingleTrackGroupInstance : MainInterface->GetCutTimeline()->TrackGroupInstances) { TSharedPtr TrackHead = StaticCastSharedPtr(SingleTrackGroupInstance.Head); @@ -33,6 +33,10 @@ void DragDropOperator::UpdateClipProcess(ICutMainWidgetInterface* MainInterface, for (int32 i = TrackHead->TrackData.ClipData.Num() - 1; i >= 0; i--) { + if (TrackHead->TrackData.ClipData[i].ClipEndFrame > MaxLength) + { + MaxLength = TrackHead->TrackData.ClipData[i].ClipEndFrame; + } if (TimelineClip.ClipStartFrame == TrackHead->TrackData.ClipData[i].ClipStartFrame || TimelineClip.ClipEndFrame == TrackHead->TrackData.ClipData[i].ClipEndFrame) { continue; @@ -102,6 +106,8 @@ void DragDropOperator::UpdateClipProcess(ICutMainWidgetInterface* MainInterface, TimelineClip.UpdateGradientCursor(); } + FGlobalData::TrackLength = MaxLength + 30; + MainInterface->GetCutTimeline()->UpdateTimelineLength(); return; } @@ -518,6 +524,9 @@ void DragDropOperator::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& CloseCursorDecorator(); const auto& DragDropOperation = static_cast(DragDropEvent.GetOperation().ToSharedRef().Get()); + + FSlateApplication::Get().SetKeyboardFocus(static_cast(SavedMainInterface)->AsShared()); + if (DragDropOperation.DragDropType == FCutDragDropBase::EType::ClipsMove) { TSharedPtr ClipMove = DragDropEvent.GetOperationAs(); @@ -706,14 +715,21 @@ void DragDropOperator::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& TSharedPtr TrackHead = TrackBody->TrackHead; TSharedPtr CutDragDropBase = DragDropEvent.GetOperationAs(); + if (CutDragDropBase->TrackType == ETrackType::None) + { + return; + } FTrackData NewTrackData; NewTrackData.TrackType = CutDragDropBase->TrackType; NewTrackData.TrackName = CutDragDropBase->DeviceName; - FDeviceTrack NewDeviceTrack; NewDeviceTrack.DeviceName = CutDragDropBase->DeviceName; NewDeviceTrack.DeviceType = CutDragDropBase->TrackType; - + + if (TrackHead->GroupName == TEXT("固定轨道")) + { + return; + } CutDragDropBase->MainInterface->GetCutTimeline()->AddNewDeviceToGroup(TrackHead->GroupName, NewDeviceTrack); CutDragDropBase->MainInterface->OnAddNewTrack(ETrackType::PlayerTrack); return; @@ -886,6 +902,7 @@ void DragDropOperator::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& return; } NewClipData.ClipType = TrackHead->TrackData.TrackType; + NewClipData.BindTrackGuid = TrackHead->TrackData.DeviceTrack.Guid; TSharedPtr OriginTrackBody = StaticCastSharedPtr(TimelineClip->Body); OriginTrackBody->TrackHead->TrackData.ClipData.Remove(NewClipData); TrackHead->TrackData.ClipData.Add(NewClipData); diff --git a/Source/Cut5/Widgets/MicroWidgets/SColorPanel.cpp b/Source/Cut5/Widgets/MicroWidgets/SColorPanel.cpp index 2547972..8723363 100644 --- a/Source/Cut5/Widgets/MicroWidgets/SColorPanel.cpp +++ b/Source/Cut5/Widgets/MicroWidgets/SColorPanel.cpp @@ -19,7 +19,7 @@ void SColorPanel::Construct(const FArguments& InArgs) CurrentSelectColor = ColorPtr->LinearRGBToHSV(); ColorS = ColorPtr->LinearRGBToHSV().G; FTextBlockStyle TextBlockStyle = FAppStyle::GetWidgetStyle("NormalText"); - TextBlockStyle.SetFontSize(16); + TextBlockStyle.SetFontSize(13); ChildSlot [ SNew(SOverlay) diff --git a/Source/Cut5/Widgets/SCustomInputPanel.cpp b/Source/Cut5/Widgets/SCustomInputPanel.cpp index 5b91249..deb8cab 100644 --- a/Source/Cut5/Widgets/SCustomInputPanel.cpp +++ b/Source/Cut5/Widgets/SCustomInputPanel.cpp @@ -324,7 +324,7 @@ void SCustomInputPanel::Construct(const FArguments& InArgs) - AddPreset(TEXT("结束后常亮"), TEXT(""), EPresetType::Custom); + // AddPreset(TEXT("结束后常亮"), TEXT(""), EPresetType::Custom); AddPreset(TEXT("渐变"), TEXT(""), EPresetType::Custom, FPresetsCustomData::EPresetCustomType::Gradient); AddPreset(TEXT("亮白"), TEXT("亮白.dat"), EPresetType::Custom); AddPreset(TEXT("红色"), TEXT("红色.dat"), EPresetType::Custom); diff --git a/Source/Cut5/Widgets/SCutMainWindow.cpp b/Source/Cut5/Widgets/SCutMainWindow.cpp index 1c205fc..75f5695 100644 --- a/Source/Cut5/Widgets/SCutMainWindow.cpp +++ b/Source/Cut5/Widgets/SCutMainWindow.cpp @@ -392,6 +392,7 @@ void SCutMainWindow::Construct(const FArguments& InArgs) CommandList->MapAction(FShortCutCommands::Get().PlayFrame, FExecuteAction::CreateLambda([this]() { CutTimeline->SetAutoPlay(!CutTimeline->AutoPlaying); + CutTimeline->PlayImage->SetImage(FUtils::GetBrushFromImage(FUtils::GetResourcesPath(CutTimeline->AutoPlaying ? "ColorSelectCircle.png" : "PlayButton.png"), {24, 24})); })); CommandList->MapAction(FShortCutCommands::Get().Delete, FExecuteAction::CreateLambda([this]() { @@ -566,6 +567,11 @@ void SCutMainWindow::Construct(const FArguments& InArgs) FReply SCutMainWindow::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { + if (HasMouseCapture()) + { + GEngine->GameViewport->GetWindow()->MoveWindowTo(GEngine->GameViewport->GetWindow()->GetPositionInScreen() + MouseEvent.GetCursorDelta()); + } + this->NewMouseEvent = MouseEvent; return SCompoundWidget::OnMouseMove(MyGeometry, MouseEvent); } @@ -680,12 +686,34 @@ FReply SCutMainWindow::OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& return FReply::Handled().EndDragDrop(); } +void SCutMainWindow::CloseAllThreads() +{ + for (int32 i = 0; i < Threads.Num(); i++) + { + TArray Guids; + Threads.GetKeys(Guids); + for (int32 j = 0; j < Guids.Num(); j++) + { + Threads[Guids[j]]->Stop(); + delete Threads[Guids[j]]; + Threads.Remove(Guids[j]); + } + } +} void SCutMainWindow::OnUpdateVideo(FGuid UUID, int32 X, int32 Y, uint8* RawData) { - ICutMainWidgetInterface::OnUpdateVideo(UUID, X, Y, RawData); - StatePanel->VideoPlayer->UpdateVideoData(UUID, X, Y, RawData); + if (!IsInGameThread()) + { + + } + else + { + ICutMainWidgetInterface::OnUpdateVideo(UUID, X, Y, RawData); + StatePanel->VideoPlayer->UpdateVideoData(UUID, X, Y, RawData); + } + } void SCutMainWindow::OnUpdateLightArray(const TArray& LightArray) @@ -797,7 +825,7 @@ void SCutMainWindow::OpenTimeline(const FString& TimelineName, bool NeedSaveBefo if (CutTimeline->LoadTimeline(TimelineName, TimelineInfo)) { CutTimeline->TimelineInfo = TimelineInfo; - CutTimeline->CurrentEditDebug->SetText(FText::FromString(TEXT("当前正在编辑") + TimelineInfo.CurrentOpenFullPath)); + // CutTimeline->CurrentEditDebug->SetText(FText::FromString(TEXT("当前正在编辑") + TimelineInfo.CurrentOpenFullPath)); } OnAddNewTrack(ETrackType::PlayerTrack); @@ -989,7 +1017,8 @@ void SCutMainWindow::ExportProject(const FString& ExportPath) } } } - + + bool bIsLightBar = false; for (FDeviceTrackGroup& DeviceTrackGroup : CutTimeline->DeviceTrackGroups) { for (FDeviceTrack& TrackData : DeviceTrackGroup.DeviceTracks) @@ -1031,14 +1060,21 @@ void SCutMainWindow::ExportProject(const FString& ExportPath) DMLight->InsertNewChildElement("Name")->InsertNewText(TCHAR_TO_UTF8(*TrackData.DeviceName)); DeviceID++; IDList.Add(TrackData.Guid, DeviceID); + break; } case ETrackType::LightBarTrack: { + if (bIsLightBar) + { + break; + } tinyxml2::XMLElement* LightArray = LightArrayList->InsertNewChildElement("GuangZhen"); LightArray->InsertNewChildElement("ID")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(DeviceID))); LightArray->InsertNewChildElement("Name")->InsertNewText(TCHAR_TO_UTF8(*TrackData.DeviceName)); DeviceID++; IDList.Add(TrackData.Guid, DeviceID); + bIsLightBar = true; + break; } default: break; @@ -1131,7 +1167,7 @@ void SCutMainWindow::ExportProject(const FString& ExportPath) } // KeyBoard - tinyxml2::XMLElement* Keyboard = RootElement->InsertNewChildElement("KeyBoard"); + tinyxml2::XMLElement* Keyboard = DeviceList->InsertNewChildElement("KeyBoard"); { tinyxml2::XMLElement* KeyCode = Keyboard->InsertNewChildElement("KeyCode"); KeyCode->InsertNewChildElement("CTRL")->InsertNewText(""); @@ -1355,8 +1391,12 @@ void SCutMainWindow::DeleteAllAssetsInTimeline() void SCutMainWindow::PreSettingBeforeSeek() { // OnUpdateProjector(0, true); - OnUpdateVideo(FGuid::NewGuid(), 1920, 1080, nullptr); + // OnUpdateVideo(FGuid::NewGuid(), 1920, 1080, nullptr); OnUpdateSpotLight(0, FColor(255, 255, 255 ,255)); + for (int32 i = 0; i < CutTimeline->TrackGroupInstances.Num(); i++) + { + OnUpdatePlayers(CutTimeline->TrackGroupInstances[i].Body, FColor(255, 255, 255, 255)); + } } void SCutMainWindow::OpenColorPanel(FLinearColor* ColorPtr) @@ -1390,30 +1430,35 @@ ESelectMode SCutMainWindow::GetSelectedMode() return SelectMode; } +SCutMainWindow* SCutMainWindow::GetSelf() +{ + return this; +} + FReply SCutMainWindow::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (CommandList->ProcessCommandBindings(InKeyEvent)) { - return FReply::Handled(); + return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent); } - return FReply::Handled(); + return SCompoundWidget::OnKeyDown(MyGeometry, InKeyEvent); +} + +FReply SCutMainWindow::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) +{ + + return FReply::Handled().ReleaseMouseCapture(); } FReply SCutMainWindow::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { - if (CommandList->ProcessCommandBindings(MouseEvent)) - { - return FReply::Handled(); - } - return FReply::Handled(); + + return FReply::Handled().CaptureMouse(SharedThis(this)); } FReply SCutMainWindow::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { - if (CommandList->ProcessCommandBindings(MouseEvent)) - { - return FReply::Handled(); - } + return FReply::Handled(); } @@ -1454,7 +1499,9 @@ tinyxml2::XMLElement* SCutMainWindow::GetDeviceElement(tinyxml2::XMLElement* Par } if (Count == 0) { - Event_List->InsertNewText(""); + tinyxml2::XMLElement* Event = Event_List->InsertNewChildElement("Event"); + Event->InsertNewChildElement("Value")->InsertNewText("0"); + Event->InsertNewChildElement("TimeCode")->InsertNewText("0"); } } } @@ -1473,7 +1520,14 @@ tinyxml2::XMLElement* SCutMainWindow::GetDeviceElement(tinyxml2::XMLElement* Par tinyxml2::XMLElement* SpeicalEffect = PlayerLight->InsertNewChildElement("Special_Effects_List"); if (TrackData.ClipData.Num() == 0) { - SpeicalEffect->InsertNewText(""); + // Default + tinyxml2::XMLElement* NewSpeicalEffect = SpeicalEffect->InsertNewChildElement("Special_Effect"); + NewSpeicalEffect->InsertNewChildElement("Mode")->InsertNewText("0"); + NewSpeicalEffect->InsertNewChildElement("InitialColor")->InsertNewText("106090"); + NewSpeicalEffect->InsertNewChildElement("EndColor")->InsertNewText("000000"); + NewSpeicalEffect->InsertNewChildElement("TimeLength")->InsertNewText("-1"); + NewSpeicalEffect->InsertNewChildElement("TimeCode")->InsertNewText("0"); + NewSpeicalEffect->InsertNewChildElement("Cycle")->InsertNewText("0"); } for (int32 k = 0; k < TrackData.ClipData.Num(); k++) { @@ -1501,6 +1555,27 @@ tinyxml2::XMLElement* SCutMainWindow::GetDeviceElement(tinyxml2::XMLElement* Par NewSpeicalEffect->InsertNewChildElement("TimeCode")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%ls"), *FUtils::GetMsFromString(FGlobalData::GetTimeData(TempClipData.ClipStartFrame))))); NewSpeicalEffect->InsertNewChildElement("Cycle")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%d"), TempClipData.PresetsCustomData.Times))); } + if (TempClipData.PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Gradient) + { + tinyxml2::XMLElement* NewSpeicalEffect = SpeicalEffect->InsertNewChildElement("Special_Effect"); + NewSpeicalEffect->InsertNewChildElement("Mode")->InsertNewText("0"); + NewSpeicalEffect->InsertNewChildElement("InitialColor")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%ls"), *FUtils::Color2Hex3(TempClipData.PresetsCustomData.Cursors[0].Color.ToFColor(false))))); + NewSpeicalEffect->InsertNewChildElement("EndColor")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%ls"), *FUtils::Color2Hex3(TempClipData.PresetsCustomData.Cursors[1].Color.ToFColor(false))))); + float PerLength = (TempClipData.PresetsCustomData.Time / TempClipData.PresetsCustomData.Times) / 2; + NewSpeicalEffect->InsertNewChildElement("TimeLength")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%d"), int32(PerLength * 1000)))); + NewSpeicalEffect->InsertNewChildElement("TimeCode")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%ls"), *FUtils::GetMsFromString(FGlobalData::GetTimeData(TempClipData.ClipStartFrame))))); + NewSpeicalEffect->InsertNewChildElement("Cycle")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%d"), TempClipData.PresetsCustomData.Times))); + } + if (TempClipData.PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::None) + { + tinyxml2::XMLElement* NewSpeicalEffect = SpeicalEffect->InsertNewChildElement("Special_Effect"); + NewSpeicalEffect->InsertNewChildElement("Mode")->InsertNewText("0"); + NewSpeicalEffect->InsertNewChildElement("InitialColor")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%ls"), *FUtils::Color2Hex3(TempClipData.PresetsCustomData.Colors[0].ToFColor(false))))); + NewSpeicalEffect->InsertNewChildElement("EndColor")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%ls"), *FUtils::Color2Hex3(TempClipData.PresetsCustomData.Colors[0].ToFColor(false))))); + NewSpeicalEffect->InsertNewChildElement("TimeLength")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%d"), -1))); + NewSpeicalEffect->InsertNewChildElement("TimeCode")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%ls"), *FUtils::GetMsFromString(FGlobalData::GetTimeData(TempClipData.ClipStartFrame))))); + NewSpeicalEffect->InsertNewChildElement("Cycle")->InsertNewText(TCHAR_TO_UTF8(*FString::Printf(TEXT("%d"), 1))); + } } j++; } @@ -1584,7 +1659,15 @@ tinyxml2::XMLElement* SCutMainWindow::GetVideoElement(tinyxml2::XMLElement* Pare tinyxml2::XMLElement* Video = Parent->InsertNewChildElement("Video"); tinyxml2::XMLElement* URL = Video->InsertNewChildElement("URL"); { - URL->InsertNewText(TCHAR_TO_UTF8(*(FPaths::GetBaseFilename(EncodeVideoInfo.EncodedVideoName) + TEXT(".mp4")))); + if (EncodeVideoInfo.EncodedVideoName == "") + { + URL->InsertNewText(""); + } + else + { + URL->InsertNewText(TCHAR_TO_UTF8(*(FPaths::GetBaseFilename(EncodeVideoInfo.EncodedVideoName) + TEXT(".mp4")))); + } + } tinyxml2::XMLElement* Timecode = Video->InsertNewChildElement("Timecode"); { @@ -1598,11 +1681,7 @@ tinyxml2::XMLElement* SCutMainWindow::GetVideoElement(tinyxml2::XMLElement* Pare { Mode->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(0))); } - tinyxml2::XMLElement* ProjectorID = Video->InsertNewChildElement("ProjectorID"); - { - ProjectorID->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(TempProjectorID))); - } - + tinyxml2::XMLElement* VolumeEventList = Video->InsertNewChildElement("VolumeEventList"); { tinyxml2::XMLElement* VolumeEvent = VolumeEventList->InsertNewChildElement("VolumeEvent"); @@ -1625,6 +1704,13 @@ tinyxml2::XMLElement* SCutMainWindow::GetVideoElement(tinyxml2::XMLElement* Pare } } + + tinyxml2::XMLElement* ProjectorID = Video->InsertNewChildElement("ProjectorID"); + { + ProjectorID->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(TempProjectorID))); + } + + tinyxml2::XMLElement* ProjectorEventList = Video->InsertNewChildElement("ProjectorEventList"); { @@ -1681,12 +1767,17 @@ tinyxml2::XMLElement* SCutMainWindow::GetSoundElement(tinyxml2::XMLElement* Pare tinyxml2::XMLElement* Sound = Parent->InsertNewChildElement("Sound"); { Sound->InsertNewChildElement("RotationSpeakerID")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(RotatorSpeakerIndex))); - Sound->InsertNewChildElement("URL")->InsertNewText(TCHAR_TO_UTF8(*(FPaths::GetBaseFilename(EncodeVideoInfo.EncodedVideoName) + TEXT(".mp3")))); + if (EncodeVideoInfo.EncodedVideoName == "") + { + Sound->InsertNewChildElement("URL")->InsertNewText(""); + } + else + { + Sound->InsertNewChildElement("URL")->InsertNewText(TCHAR_TO_UTF8(*(FPaths::GetBaseFilename(EncodeVideoInfo.EncodedVideoName) + TEXT(".mp3")))); + } - Sound->InsertNewChildElement("Loop")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(0))); - Sound->InsertNewChildElement("Mode")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(0))); - Sound->InsertNewChildElement("Round")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(0))); - Sound->InsertNewChildElement("Timecode")->InsertNewText(TCHAR_TO_UTF8(*FUtils::GetMsFromString(EncodeVideoInfo.EncodedVideoTimeCode))); + + tinyxml2::XMLElement* VolumeEventList = Sound->InsertNewChildElement("VolumeEventList"); { tinyxml2::XMLElement* VolumeEvent = VolumeEventList->InsertNewChildElement("VolumeEvent"); @@ -1707,6 +1798,12 @@ tinyxml2::XMLElement* SCutMainWindow::GetSoundElement(tinyxml2::XMLElement* Pare } } } + + Sound->InsertNewChildElement("Loop")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(0))); + Sound->InsertNewChildElement("Mode")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(0))); + Sound->InsertNewChildElement("Round")->InsertNewText(TCHAR_TO_UTF8(*FString::FromInt(1))); + Sound->InsertNewChildElement("Timecode")->InsertNewText(TCHAR_TO_UTF8(*FUtils::GetMsFromString(EncodeVideoInfo.EncodedVideoTimeCode))); + tinyxml2::XMLElement* RotationSpeakerEventList = Sound->InsertNewChildElement("RotationSpeakerEventList"); { tinyxml2::XMLElement* RotationSpeakerEvent = RotationSpeakerEventList->InsertNewChildElement("RotationSpeakerEvent"); @@ -1716,7 +1813,7 @@ tinyxml2::XMLElement* SCutMainWindow::GetSoundElement(tinyxml2::XMLElement* Pare RotationSpeakerEventTimeCode->InsertNewText("0"); - RotationSpeakerEventValue->InsertNewText("0"); + RotationSpeakerEventValue->InsertNewText("1"); } } } @@ -1772,7 +1869,7 @@ tinyxml2::XMLElement* SCutMainWindow::GetSoundListElement(tinyxml2::XMLElement* } if (Count == 0) { - AudioList->InsertNewText(""); + GetSoundElement(AudioList, FEncodeVideoInfo()); } } return nullptr; @@ -1787,6 +1884,10 @@ tinyxml2::XMLElement* SCutMainWindow::GetProcessA(tinyxml2::XMLElement* Parent, OpenTimeline(CurtainGroup->Curtains[i].TimelineInfo.CurrentOpenFullPath, true, true); GetProcessB(ProcessA, &CurtainGroup->Curtains[i]); } + if (CurtainGroup->Curtains.Num() == 0) + { + ProcessA->InsertNewText(""); + } return ProcessA; } @@ -1805,7 +1906,9 @@ tinyxml2::XMLElement* SCutMainWindow::GetProcessB(tinyxml2::XMLElement* Parent, // 非必须项 tinyxml2::XMLElement* Identity_SpecialEffects = ProcessB->InsertNewChildElement("Identity_SpecialEffects"); { - Identity_SpecialEffects->InsertNewText(""); + tinyxml2::XMLElement* SpecialEffects = Identity_SpecialEffects->InsertNewChildElement("SpecialEffects"); + SpecialEffects->InsertNewChildElement("PlayerID")->InsertNewText("1"); + SpecialEffects->InsertNewChildElement("SpecialEffectID")->InsertNewText(""); } tinyxml2::XMLElement* IsGlobal = ProcessB->InsertNewChildElement("IsGlobal"); { @@ -1813,7 +1916,7 @@ tinyxml2::XMLElement* SCutMainWindow::GetProcessB(tinyxml2::XMLElement* Parent, } tinyxml2::XMLElement* State = ProcessB->InsertNewChildElement("State"); { - State->InsertNewText(""); + State->InsertNewText("0"); } tinyxml2::XMLElement* EnableCard = ProcessB->InsertNewChildElement("EnableCard"); { @@ -1896,6 +1999,9 @@ tinyxml2::XMLElement* SCutMainWindow::GetSpecialEffectGroup(tinyxml2::XMLElement } tinyxml2::XMLElement* AutoNext = Effect->InsertNewChildElement("AutoNext"); AutoNext->InsertNewText("0"); + + Effect->InsertNewChildElement("TimeLength")->InsertNewText("-1"); + GetSoundListElement(Effect); GetDeviceElement(Effect); GetVideoListElement(Effect); @@ -1909,6 +2015,9 @@ tinyxml2::XMLElement* SCutMainWindow::GetSpecialEffectGroup(tinyxml2::XMLElement { State->InsertNewText("0"); } + + GetTrigger(SpecialEffect); + return Effect; } @@ -1924,6 +2033,7 @@ tinyxml2::XMLElement* SCutMainWindow::GetSpecialEffect(tinyxml2::XMLElement* Par } tinyxml2::XMLElement* AutoNext = Effectxml->InsertNewChildElement("AutoNext"); + Effectxml->InsertNewChildElement("TimeLength")->InsertNewText("-1"); AutoNext->InsertNewText("0"); GetSoundListElement(Effectxml); GetDeviceElement(Effectxml); @@ -1938,9 +2048,26 @@ tinyxml2::XMLElement* SCutMainWindow::GetSpecialEffect(tinyxml2::XMLElement* Par { State->InsertNewText("0"); } + + GetTrigger(SpecialEffect); return Effectxml; } +tinyxml2::XMLElement* SCutMainWindow::GetTrigger(tinyxml2::XMLElement* Parent) +{ + tinyxml2::XMLElement* Trigger = Parent->InsertNewChildElement("Trigger"); + Trigger->InsertNewChildElement("TriggerMode")->InsertNewText(""); + Trigger->InsertNewChildElement("SerialNumberList")->InsertNewChildElement("SerialNumber")->InsertNewText(""); + Trigger->InsertNewChildElement("SerialNumber")->InsertNewText(""); + tinyxml2::XMLElement* KeyCode = Trigger->InsertNewChildElement("KeyCode"); + KeyCode->InsertNewChildElement("CTRL")->InsertNewText(""); + KeyCode->InsertNewChildElement("SHIFT")->InsertNewText(""); + KeyCode->InsertNewChildElement("ALT")->InsertNewText(""); + KeyCode->InsertNewChildElement("KEY")->InsertNewText(""); + Trigger->InsertNewChildElement("ActionMode")->InsertNewText(""); + return Trigger; +} + int32 SCutMainWindow::GetTrackID(FGuid Guid) const { const int32* Index = IDList.Find(Guid); @@ -1948,5 +2075,29 @@ int32 SCutMainWindow::GetTrackID(FGuid Guid) const return NewIndex; } +FRunnable* SCutMainWindow::GetThread(const FGuid& Guid) +{ + if (!Threads.Find(Guid)) + { + return nullptr; + } + return *Threads.Find(Guid); +} + +void SCutMainWindow::AddThread(const FGuid& Guid, FRunnable* InSoundThread) +{ + if (!Threads.Find(Guid)) + { + Threads.Add(Guid, InSoundThread); + } + else + { + FRunnable* Thread = *Threads.Find(Guid); + Thread->Stop(); + delete Thread; + Threads[Guid] = InSoundThread; + } +} + END_SLATE_FUNCTION_BUILD_OPTIMIZATION diff --git a/Source/Cut5/Widgets/SCutMainWindow.h b/Source/Cut5/Widgets/SCutMainWindow.h index 61b8629..cd69e07 100644 --- a/Source/Cut5/Widgets/SCutMainWindow.h +++ b/Source/Cut5/Widgets/SCutMainWindow.h @@ -50,8 +50,13 @@ public: virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; virtual FReply OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; virtual FReply OnDrop(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; + + + + + void CloseAllThreads(); FSoundThread* SoundThread; - + float TotalTime; bool bRenderLine = false; @@ -89,10 +94,12 @@ public: virtual void OpenColorPanel(FLinearColor* ColorPtr); virtual void AddNewCustomPreset(const FString& Name, const FPresetsCustomData CustomData) override; virtual ESelectMode GetSelectedMode() override; + virtual SCutMainWindow* GetSelf() override; FPointerEvent NewMouseEvent; virtual bool SupportsKeyboardFocus() const override { return true; }; virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override; + virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; virtual FReply OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; @@ -108,12 +115,17 @@ public: tinyxml2::XMLElement* GetSpecialEffectList(tinyxml2::XMLElement* Parent); tinyxml2::XMLElement* GetSpecialEffectGroup(tinyxml2::XMLElement* Parent, FEffectCardGroup* Group); tinyxml2::XMLElement* GetSpecialEffect(tinyxml2::XMLElement* Parent, FEffectCardProperty* Effect); - + tinyxml2::XMLElement* GetTrigger(tinyxml2::XMLElement* Parent); + int32 RotatorSpeakerIndex = 0; int32 LightArrayIndex = 0; TMap IDList = {}; int32 GetTrackID(FGuid Guid) const; + + TMap Threads; + FRunnable* GetThread(const FGuid& Guid); + void AddThread(const FGuid& Guid, FRunnable* InSoundThread); }; diff --git a/Source/Cut5/Widgets/SCutTimeline.cpp b/Source/Cut5/Widgets/SCutTimeline.cpp index 7873ed0..f01d549 100644 --- a/Source/Cut5/Widgets/SCutTimeline.cpp +++ b/Source/Cut5/Widgets/SCutTimeline.cpp @@ -3,6 +3,7 @@ #include "SCutTimeline.h" +#include "SCutMainWindow.h" #include "SlateOptMacros.h" #include "STimelineTick.h" #include "STrackBody.h" @@ -91,6 +92,7 @@ int32 SCutTimeline::GetCursorPosition() const void SCutTimeline::Construct(const FArguments& InArgs) { + MainWidgetInterface = InArgs._MainWidgetInterface; ChildSlot [ @@ -150,8 +152,7 @@ void SCutTimeline::Construct(const FArguments& InArgs) + SHorizontalBox::Slot() .FillWidth(300) .AutoWidth() - - + + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) @@ -173,32 +174,50 @@ void SCutTimeline::Construct(const FArguments& InArgs) .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) - .SizeParam(FAuto()) [ - SNew(SImage) - .Image(FUtils::GetBrushFromImage(FUtils::GetResourcesPath("PlayButton.png"), {})) + SNew(SBox) + .WidthOverride(24) + .HeightOverride(24) + [ + SAssignNew(PlayImage, SImage) + .Image(FUtils::GetBrushFromImage(FUtils::GetResourcesPath("PlayButton.png"), {24, 24})) + .OnMouseButtonDown_Lambda([this](const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) + { + SetAutoPlay(!AutoPlaying); + PlayImage->SetImage(FUtils::GetBrushFromImage(FUtils::GetResourcesPath(AutoPlaying ? "ColorSelectCircle.png" : "PlayButton.png"), {24, 24})); + return FReply::Handled(); + }) + + ] ] + SHorizontalBox::Slot() - .SizeParam(FStretch(1.0)) [ - SAssignNew(ZoomSlider, SSlider) - .MaxValue(1.0) - .MinValue(0.0) - .Value(0.0f) - .OnValueChanged_Lambda([this](float ChangedValue) - { - FGlobalData::DefaultTimeTickSpace = FMath::GetMappedRangeValueClamped(FVector2D(0, 1.0), FVector2D(GetCachedGeometry().GetLocalSize().X / FGlobalData::TrackLength, 14), ChangedValue); - UpdateCursorPosition(GetCursorPosition()); - RenderGroup(); - }) - ] - + SHorizontalBox::Slot() - .SizeParam(FAuto()) - [ - SAssignNew(CurrentEditDebug, STextBlock) - .Text(FText::FromString(TEXT("当前正在编辑主面板"))) - + SNew(SBox) + .WidthOverride(200) + [ + SAssignNew(ZoomSlider, SSlider) + .MaxValue(1.0) + .MinValue(0.0) + .Value_Lambda([this]() + { + return FMath::GetMappedRangeValueClamped(FVector2D(GetCachedGeometry().GetLocalSize().X / FGlobalData::TrackLength, 14), FVector2D(0, 1.0), FGlobalData::DefaultTimeTickSpace); + }) + .OnValueChanged_Lambda([this](float ChangedValue) + { + FGlobalData::DefaultTimeTickSpace = FMath::GetMappedRangeValueClamped(FVector2D(0, 1.0), FVector2D(GetCachedGeometry().GetLocalSize().X / FGlobalData::TrackLength, 14), ChangedValue); + UpdateCursorPosition(GetCursorPosition()); + RenderGroup(); + }) + ] + ] + // + SHorizontalBox::Slot() + // .SizeParam(FAuto()) + // [ + // SAssignNew(CurrentEditDebug, STextBlock) + // .Text(FText::FromString(TEXT("当前正在编辑主面板"))) + // + // ] ] ] @@ -366,23 +385,30 @@ void SCutTimeline::Construct(const FArguments& InArgs) AddNewDeviceToGroup(TEXT("固定轨道"), LightBarData2); FDeviceTrack SpotLightData(TEXT("投射灯一"), ETrackType::SpotLightTrack); AddNewDeviceToGroup(TEXT("固定轨道"), SpotLightData); + + + + FTickCursorTimeThread* TickCursorTimeThread = new FTickCursorTimeThread(FGlobalData::GlobalFPS, [this]() + { + UpdateCursorPosition(GetCursorPosition() + 1); + }); + FRunnableThread::Create(TickCursorTimeThread, TEXT("TickCursorTimeThread")); + } void SCutTimeline::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { - SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); + if (AutoPlaying) { TimeSpace += InDeltaTime; - if (TimeSpace >= 1.0 / FGlobalData::GlobalFPS) + if (TimeSpace >= (1.0 / FGlobalData::GlobalFPS)) { - UpdateCursorPosition(GetCursorPosition() + 1); + TimeSpace = 0.0; } - - - } + SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); } bool FSingleTrackGroupInstance::IsThis(TSharedPtr WidgetInterface) const @@ -428,6 +454,15 @@ void SCutTimeline::AddNewTrackToGroup(FString GroupName, FTrackData TrackData, E void SCutTimeline::SetAutoPlay(bool bStart) { AutoPlaying = bStart; + if (bStart) + { + ClockRun = true; + } + else + { + ClockRun = false; + } + } int32 SCutTimeline::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, @@ -833,6 +868,10 @@ void SCutTimeline::AddSelectedClipsOffset(int32 FrameOffset) ClipData->ClipStartFrame += FrameOffset; ClipData->ClipEndFrame += FrameOffset; } + for (FClipData* ClipData : NewSelectedClipData) + { + DragDropOperator::GetDragDropOperator()->UpdateClipProcess(MainWidgetInterface, *ClipData); + } RenderGroup(); } } @@ -845,6 +884,11 @@ void SCutTimeline::AddSelectedClipsOffset(int32 FrameOffset) ClipData->ClipStartFrame += FrameOffset; ClipData->ClipEndFrame += FrameOffset; } + for (FClipData* ClipData : NewSelectedClipData) + { + DragDropOperator::GetDragDropOperator()->UpdateClipProcess(MainWidgetInterface, *ClipData); + } + RenderGroup(); } } diff --git a/Source/Cut5/Widgets/SCutTimeline.h b/Source/Cut5/Widgets/SCutTimeline.h index 41806fb..0bd4605 100644 --- a/Source/Cut5/Widgets/SCutTimeline.h +++ b/Source/Cut5/Widgets/SCutTimeline.h @@ -17,6 +17,9 @@ * */ + +static inline std::atomic ClockRun = false; + struct FTrackGroup { // TrackGroup Mean Device @@ -66,8 +69,11 @@ public: void Construct(const FArguments& InArgs); virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override; float TimeSpace = 0.0f; + float LastTime = 0.1f; void AddNewTrackToGroup(FString GroupName, FTrackData TrackData, ETrackType GroupType = ETrackType::None); void SetAutoPlay(bool bStart); + + FTimerHandle TimerHandle; bool AutoPlaying = false; virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; @@ -113,6 +119,7 @@ public: FTimelineInfo TimelineInfo; FGuid SelectedClipGUID; + TSharedPtr PlayImage; /** * @brief Selected Clips Guid, Use for Multi-Select. @@ -140,3 +147,45 @@ public: }; + +class FTickCursorTimeThread final : public FRunnable +{ +public: + FTickCursorTimeThread(int32 InFPS, TFunction InBindFunction) + { + FPS = InFPS; + BindFunction = InBindFunction; + } + int32 FPS = 0; + TFunction BindFunction; + bool bIsDead = false; + virtual uint32 Run() override + { + while (!bIsDead) + { + if (ClockRun) + { + FPlatformProcess::Sleep(1.0f / FPS); + FFunctionGraphTask::CreateAndDispatchWhenReady([this]() + { + if (BindFunction) + BindFunction(); + }, TStatId(), nullptr, ENamedThreads::GameThread); + } + } + return 0; + }; + virtual void Stop() override + { + bIsDead = true; + }; + virtual void Exit() override + { + + }; + virtual bool Init() override + { + return true; + }; +}; + diff --git a/Source/Cut5/Widgets/STimelineClip.cpp b/Source/Cut5/Widgets/STimelineClip.cpp index bae4d92..aac6808 100644 --- a/Source/Cut5/Widgets/STimelineClip.cpp +++ b/Source/Cut5/Widgets/STimelineClip.cpp @@ -8,17 +8,20 @@ #include "AudioDevice.h" #include "ImageUtils.h" +#include "SCutMainWindow.h" #include "SCutTimeline.h" #include "SlateOptMacros.h" #include "STrackBody.h" #include "Commands/TimelineClipCommands.h" #include "Cut5/WidgetInterface.h" #include "Cut5/Interface/SoundInterface.h" +#include "Cut5/Interface/VideoInterface.h" #include "Cut5/Utils/FFMPEGUtils.h" #include "Cut5/Utils/Utils.h" #include "DragDropOperator/DragDropOperator.h" #include "Engine/Engine.h" #include "Engine/Texture2D.h" +#include "HAL/ThreadManager.h" #include "MicroWidgets/SNewProjectTips.h" #include "Presets/SClipCursor.h" #include "Rendering/DrawElementPayloads.h" @@ -48,6 +51,8 @@ FReply STimelineClip::OnBorderMouseButtonDown(const FGeometry& Geometry, const F FMenuBuilder MenuBuilder(true, CommandList); MenuBuilder.AddMenuEntry(FTimelineClipCommands::Get().Remove); MenuBuilder.AddMenuEntry(FTimelineClipCommands::Get().Break); + MenuBuilder.AddMenuEntry(FTimelineClipCommands::Get().Fill2Start); + MenuBuilder.AddMenuEntry(FTimelineClipCommands::Get().Fill2End); MenuContent = MenuBuilder.MakeWidget(); FSlateApplication::Get().PushMenu(AsShared(), FWidgetPath(), MenuContent.ToSharedRef(), FSlateApplication::Get().GetCursorPos(), FPopupTransitionEffect::ContextMenu); return FReply::Handled(); @@ -171,6 +176,8 @@ FReply STimelineClip::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointe return FReply::Handled(); } + + void STimelineClip::Construct(const FArguments& InArgs) { CommandList = InArgs._CommandList; @@ -202,7 +209,26 @@ void STimelineClip::Construct(const FArguments& InArgs) ]; if (ClipData->ClipType == ETrackType::AudioTrack) { - + // if (!MainWidgetInterface->GetSelf()->GetThread(ClipData->ClipGuid)) + // { + // FVideoThread* Thread = new FVideoThread([this](uint8* RawData, int32 Size, int32 X, int32 Y) + // { + // MainWidgetInterface->OnUpdateVideo(ClipData->ClipGuid, X, Y, RawData); + // }, *ClipData); + // FRunnableThread::Create(Thread, TEXT("VideoThread")); + // } + } + if (ClipData->ClipType == ETrackType::VideoTrack) + { + if (!MainWidgetInterface->GetSelf()->GetThread(FUtils::GetVideoThreadGuid(ClipData->ClipGuid))) + { + FVideoThread* Thread = new FVideoThread([this](uint8* RawData, int32 Size, int32 X, int32 Y) + { + MainWidgetInterface->OnUpdateVideo(ClipData->ClipGuid, X, Y, RawData); + }, *ClipData, MainWidgetInterface); + FRunnableThread::Create(Thread, TEXT("VideoThread")); + MainWidgetInterface->GetSelf()->AddThread(FUtils::GetVideoThreadGuid(ClipData->ClipGuid), Thread); + } } if (MainWidgetInterface->GetCutTimeline()->SelectedClipGUID == ClipData->ClipGuid) { @@ -250,96 +276,14 @@ void STimelineClip::Seek(int32 Frame) { return; } - - // FDateTime A = FDateTime::Now(); - - - - - const int32 Offset = Frame - (ClipData->ClipStartFrame); - int32 SeekMovieFrame = ClipData->VideoStartFrame + Offset; - - int32 VideoFPS = ClipData->ResourcePropertyDataPtr->VideoCodecContext->framerate.num; - if (VideoFPS < FGlobalData::GlobalFPS) + const int32 SeekMovieFrame = ClipData->VideoStartFrame + Offset; + if (MainWidgetInterface->GetSelf()->GetThread(FUtils::GetVideoThreadGuid(ClipData->ClipGuid))) { - const double Interval = FGlobalData::GlobalFPS / VideoFPS; - SeekMovieFrame = float(SeekMovieFrame) / Interval; + static_cast(MainWidgetInterface->GetSelf()->GetThread(FUtils::GetVideoThreadGuid(ClipData->ClipGuid)))->SeekFrame(SeekMovieFrame); } - else - { - VideoFPS = FGlobalData::GlobalFPS; - } - - - int64 Timestamp = av_rescale_q(float(SeekMovieFrame) / float(VideoFPS) * AV_TIME_BASE, AVRational{1, AV_TIME_BASE}, ClipData->ResourcePropertyDataPtr->Context->streams[ClipData->ResourcePropertyDataPtr->VideoStream]->time_base); - if (VideoFPS < FGlobalData::GlobalFPS || SeekMovieFrame - LastSeekFrame > 1 || SeekMovieFrame - LastSeekFrame < 0 || LastSeekFrame == 0) - { - AVRational frame_rate = ClipData->ResourcePropertyDataPtr->Context->streams[ClipData->ResourcePropertyDataPtr->VideoStream]->avg_frame_rate; - if (av_seek_frame(ClipData->ResourcePropertyDataPtr->Context, ClipData->ResourcePropertyDataPtr->VideoStream, Timestamp, AVSEEK_FLAG_BACKWARD) < 0) - { - - }; - } - LastSeekFrame = SeekMovieFrame; - - AVPacket* Packet = av_packet_alloc(); - AVFrame* AllocatedFrame = av_frame_alloc(); - - av_init_packet(Packet); - avcodec_receive_frame(ClipData->ResourcePropertyDataPtr->VideoCodecContext, AllocatedFrame); - int32 Times = 0; - while (av_read_frame(ClipData->ResourcePropertyDataPtr->Context, Packet) >= 0) - { - if (Packet->stream_index == ClipData->ResourcePropertyDataPtr->VideoStream) - { - int32 Response = avcodec_send_packet(ClipData->ResourcePropertyDataPtr->VideoCodecContext, Packet); - if (Response < 0) - { - UE_LOG(LogTemp, Error, TEXT("Error while sending a packet to the decoder: %s"), *FString(FString::FromInt(Response))); - return; - } - Response = avcodec_receive_frame(ClipData->ResourcePropertyDataPtr->VideoCodecContext, AllocatedFrame); - if (Response == AVERROR(EAGAIN) || Response == AVERROR_EOF) - { - continue; - } - else if (Response < 0) - { - UE_LOG(LogTemp, Error, TEXT("Error while receiving a frame from the decoder: %s"), *FString(FString::FromInt(Response))); - return; - } - if (AllocatedFrame->best_effort_timestamp >= Timestamp) - { - av_packet_unref(Packet); - break; - } - } - } - av_packet_unref(Packet); - - - AVCodecContext* VideoCodecContext = ClipData->ResourcePropertyDataPtr->VideoCodecContext; - - struct SwsContext* swsCtx = sws_getContext( - AllocatedFrame->width, AllocatedFrame->height, VideoCodecContext->pix_fmt, - AllocatedFrame->width, AllocatedFrame->height, AV_PIX_FMT_BGRA, - SWS_BILINEAR, NULL, NULL, NULL - ); - if (!swsCtx) - { - UE_LOG(LogTemp, Error, TEXT("Error creating swsContext")); - return; - } - uint8* RawData = new uint8[AllocatedFrame->width * AllocatedFrame->height * 4]; - uint8* dest[4] = {RawData, 0, 0, 0}; - int32 dest_linesize[4] = {AllocatedFrame->width * 4, 0, 0, 0}; - sws_scale(swsCtx, AllocatedFrame->data, AllocatedFrame->linesize, 0, AllocatedFrame->height, dest, dest_linesize); - sws_freeContext(swsCtx); - - MainWidgetInterface->OnUpdateVideo(FGuid::NewGuid(), AllocatedFrame->width, AllocatedFrame->height, RawData); DoSound(ESoundSolveType::None, Frame); - av_frame_free(&AllocatedFrame); + LastSeekFrame = SeekMovieFrame; } break; @@ -380,116 +324,34 @@ void STimelineClip::Seek(int32 Frame) const int32 Offset = Frame - ClipData->ClipStartFrame; const int32 SeekMovieFrame = ClipData->VideoStartFrame + Offset; - + if (ClipData->LightArrayData.Num() != 0 && SeekMovieFrame < ClipData->LightArrayData.Num()) + { + MainWidgetInterface->OnUpdateLightArray(ClipData->LightArrayData[SeekMovieFrame]); + break; + } if (ClipData->PresetsCustomData.PresetCustomType != FPresetsCustomData::EPresetCustomType::None) { if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Breathe) { - // 先拿到目前位置在第几个区间 - float Between = -1; - int32 SingleAreaLength = (ClipData->ClipEndFrame - ClipData->ClipStartFrame) / ClipData->PresetsCustomData.Times / 2; - if (SeekMovieFrame > 0) - { - Between = (float)SeekMovieFrame / (float)SingleAreaLength; - } - - if (Between != -1) - { - FLinearColor LinearColor = FLinearColor::Black; - FLinearColor CustomColor = ClipData->PresetsCustomData.Colors[0]; - LinearColor = FMath::Lerp((int32)Between % 2 == 0 ? CustomColor : FLinearColor::Black, (int32)Between % 2 == 0 ? FLinearColor::Black : CustomColor, Between - (int32)Between); - TArray Colors; - Colors.Init(LinearColor.ToFColor(false), FGlobalData::LightArrayX * FGlobalData::LightArrayY * 4); - MainWidgetInterface->OnUpdateLightArray(Colors); - } + MainWidgetInterface->OnUpdateLightArray(FUtils::SingleColor2ColorArray(ClipData->GetBreathColor(SeekMovieFrame))); } else if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Flash) { - float Between = -1; - int32 SingleAreaLength = (ClipData->ClipEndFrame - ClipData->ClipStartFrame) / ClipData->PresetsCustomData.Times / 2; - if (SeekMovieFrame > 0) - { - Between = (float)SeekMovieFrame / (float)SingleAreaLength; - } - - if (Between != -1) - { - TArray Colors; - Colors.Init((int32)Between % 2 == 1 ? ClipData->PresetsCustomData.Colors[0].ToFColor(false) : FLinearColor::Black.ToFColor(false), FGlobalData::LightArrayX * FGlobalData::LightArrayY * 4); - MainWidgetInterface->OnUpdateLightArray(Colors); - - } - } - - - if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::None) - { - - TArray Colors; - Colors.Init(ClipData->ClipColors[0].ToFColor(false), FGlobalData::LightArrayX * FGlobalData::LightArrayY * 4); - MainWidgetInterface->OnUpdateLightArray(Colors); - break; + MainWidgetInterface->OnUpdateLightArray(FUtils::SingleColor2ColorArray(ClipData->GetFlashColor(SeekMovieFrame))); } else if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Gradient) { - int32 Between = -1; - for (int32 i = 0; i < ClipData->PresetsCustomData.Cursors.Num() - 1; i++) - { - if (SeekMovieFrame >= ClipData->PresetsCustomData.Cursors[i].CursorFrameOffset && SeekMovieFrame <= ClipData->PresetsCustomData.Cursors[i + 1].CursorFrameOffset) - { - Between = i; - } - } - if (SeekMovieFrame >= ClipData->PresetsCustomData.Cursors[ClipData->PresetsCustomData.Cursors.Num() - 1].CursorFrameOffset) - { - Between = ClipData->PresetsCustomData.Cursors.Num() - 1; - } - if (Between != -1) - { - if (Between == ClipData->PresetsCustomData.Cursors.Num() - 1) - { - TArray Colors; - Colors.Init(ClipData->PresetsCustomData.Cursors[ClipData->PresetsCustomData.Cursors.Num() - 1].Color.ToFColor(false), FGlobalData::LightArrayX * FGlobalData::LightArrayY * 4); - MainWidgetInterface->OnUpdateLightArray(Colors); - } - else - { - TArray Colors; - Colors.Init(FLinearColor::Black.ToFColor(false), FGlobalData::LightArrayX * FGlobalData::LightArrayY * 4); - for (int32 i = 0; i < FGlobalData::LightArrayX * FGlobalData::LightArrayY; i++) - { - Colors[i] = FMath::Lerp(ClipData->PresetsCustomData.Cursors[Between].Color, ClipData->PresetsCustomData.Cursors[Between + 1].Color, (float)(SeekMovieFrame - ClipData->PresetsCustomData.Cursors[Between].CursorFrameOffset) / (float)(ClipData->PresetsCustomData.Cursors[Between + 1].CursorFrameOffset - ClipData->PresetsCustomData.Cursors[Between].CursorFrameOffset)).ToFColor(false); - } - MainWidgetInterface->OnUpdateLightArray(Colors); - } - - } - - break; + MainWidgetInterface->OnUpdateLightArray(FUtils::SingleColor2ColorArray(ClipData->GetGradientColor(SeekMovieFrame))); } + break; } - - - - - - if (SeekMovieFrame < ClipData->LightArrayData.Num()) + else if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::None) { - MainWidgetInterface->OnUpdateLightArray(ClipData->LightArrayData[SeekMovieFrame]); + MainWidgetInterface->OnUpdateLightArray(FUtils::SingleColor2ColorArray(ClipData->ClipColors[0].ToFColor(false))); } break; + } - case ETrackType::PlayerTrack: - { - const int32 Offset = Frame - ClipData->ClipStartFrame; - const int32 SeekMovieFrame = ClipData->VideoStartFrame + Offset; - if (SeekMovieFrame < ClipData->PlayerLightData.Num()) - { - MainWidgetInterface->OnUpdatePlayers(Body, ClipData->PlayerLightData[SeekMovieFrame]); - } - break; - } - break; case ETrackType::AudioTrack: { DoSound(ESoundSolveType::OnlyLeft, Frame); @@ -506,52 +368,25 @@ void STimelineClip::Seek(int32 Frame) { const int32 Offset = Frame - ClipData->ClipStartFrame; const int32 SeekMovieFrame = ClipData->VideoStartFrame + Offset; + + if (ClipData->PlayerLightData.Num() != 0 && SeekMovieFrame < ClipData->PlayerLightData.Num()) + { + MainWidgetInterface->OnUpdatePlayers(Body, ClipData->PlayerLightData[SeekMovieFrame]); + break; + } if (ClipData->PresetsCustomData.PresetCustomType != FPresetsCustomData::EPresetCustomType::None) { if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Breathe) { - // 先拿到目前位置在第几个区间 - float Between = -1; - int32 SingleAreaLength = (ClipData->ClipEndFrame - ClipData->ClipStartFrame) / ClipData->PresetsCustomData.Times / 2; - if (SeekMovieFrame > 0) - { - Between = (float)SeekMovieFrame / (float)SingleAreaLength; - } - - if (Between != -1) - { - FLinearColor LinearColor = FLinearColor::Black; - FLinearColor CustomColor = ClipData->PresetsCustomData.Colors[0]; - LinearColor = FMath::Lerp((int32)Between % 2 == 0 ? CustomColor : FLinearColor::Black, (int32)Between % 2 == 0 ? FLinearColor::Black : CustomColor, Between - (int32)Between); - // GEngine->AddOnScreenDebugMessage(-1, 0.1f, FColor::Red, FString::Printf(TEXT("Between %s"), *CustomColor.ToString())); - MainWidgetInterface->OnUpdatePlayers(Body, LinearColor.ToFColor(false)); - } + MainWidgetInterface->OnUpdatePlayers(Body, ClipData->GetBreathColor(SeekMovieFrame)); } else if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Flash) { - float Between = -1; - int32 SingleAreaLength = (ClipData->ClipEndFrame - ClipData->ClipStartFrame) / ClipData->PresetsCustomData.Times / 2; - if (SeekMovieFrame > 0) - { - Between = (float)SeekMovieFrame / (float)SingleAreaLength; - } - - if (Between != -1) - { - MainWidgetInterface->OnUpdatePlayers(Body, (int32)Between % 2 == 1 ? ClipData->PresetsCustomData.Colors[0].ToFColor(false) : FLinearColor::Black.ToFColor(false)); - } + MainWidgetInterface->OnUpdatePlayers(Body, ClipData->GetFlashColor(SeekMovieFrame)); } - - - break; - } - - - if (ClipData->PresetType == EPresetType::NotAPresets) - { - if (SeekMovieFrame < ClipData->PlayerLightData.Num()) + else if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Gradient) { - MainWidgetInterface->OnUpdatePlayers(Body, ClipData->PlayerLightData[SeekMovieFrame]); + MainWidgetInterface->OnUpdatePlayers(Body, ClipData->GetGradientColor(SeekMovieFrame)); } break; } @@ -559,35 +394,7 @@ void STimelineClip::Seek(int32 Frame) { MainWidgetInterface->OnUpdatePlayers(Body, ClipData->ClipColors[0].ToFColor(false)); } - else if (ClipData->PresetsCustomData.PresetCustomType == FPresetsCustomData::EPresetCustomType::Gradient) - { - int32 Between = -1; - for (int32 i = 0; i < ClipData->PresetsCustomData.Cursors.Num() - 1; i++) - { - if (i == 0) - { - if (SeekMovieFrame <= ClipData->PresetsCustomData.Cursors[i].CursorFrameOffset) - { - Between = i; - } - } - if (i == ClipData->PresetsCustomData.Cursors.Num() - 2) - { - if (SeekMovieFrame >= ClipData->PresetsCustomData.Cursors[i].CursorFrameOffset && SeekMovieFrame <= ClipData->PresetsCustomData.Cursors[i + 1].CursorFrameOffset) - { - Between = i; - } - } - } - if (Between != -1) - { - FLinearColor LinearColor = FLinearColor::Black; - LinearColor = FMath::Lerp(ClipData->PresetsCustomData.Cursors[Between].Color, ClipData->PresetsCustomData.Cursors[Between + 1].Color, (float)(SeekMovieFrame - ClipData->PresetsCustomData.Cursors[Between].CursorFrameOffset) / (float)(ClipData->PresetsCustomData.Cursors[Between + 1].CursorFrameOffset - ClipData->PresetsCustomData.Cursors[Between].CursorFrameOffset)); - MainWidgetInterface->OnUpdatePlayers(Body, LinearColor.ToFColor(false)); - - } - } - + break; } default: @@ -603,6 +410,16 @@ void STimelineClip::UpdatePosition(int32 StartFrame) } const int32 NewPosX = StartFrame * FGlobalData::DefaultTimeTickSpace; + if (StartFrame > 0) + { + if (ClipData->VideoStartFrame + StartFrame - ClipData->ClipStartFrame < 0) + { + return; + } + } + + + SetRenderTransform(FSlateRenderTransform(FVector2D(NewPosX, 0))); ClipDataBox->SetWidthOverride((ClipData->ClipEndFrame - StartFrame) * FGlobalData::DefaultTimeTickSpace); if (ClipData->ClipType == ETrackType::VideoTrack) @@ -630,6 +447,13 @@ void STimelineClip::UpdateLength(int32 EndFrame) { return; } + + if (ClipData->ClipType == ETrackType::VideoTrack) + { + + } + + ClipDataBox->SetWidthOverride((EndFrame - ClipData->ClipStartFrame) * FGlobalData::DefaultTimeTickSpace); if (ClipData->ClipType == ETrackType::VideoTrack) { @@ -861,22 +685,64 @@ void STimelineClip::DoSound(ESoundSolveType SolveType, int32 InFrame) TSharedPtr NewBody = StaticCastSharedPtr(Body); if (NewBody->TrackHead->TrackData.IsMute) return; - if (SoundThread == nullptr) + + + if (!static_cast(MainWidgetInterface)->GetThread(ClipData->ClipGuid)) { if (ClipData->ResourcePropertyDataPtr == nullptr) return; if (ClipData->ResourcePropertyDataPtr->AudioStream != -1) { - SoundThread = new FSoundThread(2, ClipData->ResourcePropertyDataPtr->AudioSample); + PaSampleFormat Type = paInt16; + switch (FUtils::GetFormatType(ClipData->ResourcePropertyDataPtr->SampleFormat)) + { + case 0: + { + Type = paUInt8; + break; + } + + case 1: + { + Type = paInt16; + break; + } + case 2: + { + Type = paInt32; + break; + } + case 3: + { + Type = paFloat32; + break; + } + case 4: + { + + break; + } + default: + { + Type = paInt32; + } + break; + } + SoundThread = new FSoundThread(2, ClipData->ResourcePropertyDataPtr->AudioSample, FUtils::GetFormatSampleBytesNum(ClipData->ResourcePropertyDataPtr->SampleFormat), Type); FRunnableThread* Thread = FRunnableThread::Create(SoundThread, TEXT("SoundThread")); SoundThread->CopyAudio(ClipData->ResourcePropertyDataPtr->AudioData.GetData(), ClipData->ResourcePropertyDataPtr->AudioData.Num(), SolveType); + static_cast(MainWidgetInterface)->AddThread(ClipData->ClipGuid, SoundThread); + if (MainWidgetInterface->GetCutTimeline()->AutoPlaying) + { + SoundThread->StartPlay(); + } } } const int32 Offset = InFrame - ClipData->ClipStartFrame; const int32 SeekMovieFrame = ClipData->VideoStartFrame + Offset; - if (SoundThread) + if (static_cast(MainWidgetInterface)->GetThread(ClipData->ClipGuid)) { - SoundThread->SeekFrame(SeekMovieFrame); + static_cast(MainWidgetInterface->GetSelf()->GetThread(ClipData->ClipGuid))->SeekFrame(SeekMovieFrame); } } diff --git a/Source/Cut5/Widgets/STimelineClip.h b/Source/Cut5/Widgets/STimelineClip.h index 17075b2..21a6b60 100644 --- a/Source/Cut5/Widgets/STimelineClip.h +++ b/Source/Cut5/Widgets/STimelineClip.h @@ -37,6 +37,7 @@ public: FReply OnBorderMouseButtonDown(const FGeometry& Geometry, const FPointerEvent& PointerEvent); FReply OnBorderMouseButtonUp(const FGeometry& Geometry, const FPointerEvent& PointerEvent); virtual FReply OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; + /** Constructs this widget with InArgs */ void Construct(const FArguments& InArgs); FClipData* ClipData; @@ -55,10 +56,10 @@ public: virtual FReply OnDragOver(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; TSharedPtr ClipOverlay; TSharedPtr CommandList; - FSoundThread* SoundThread; + void DoSound(ESoundSolveType SolveType, int32 InFrame); - + FSoundThread* SoundThread = nullptr; virtual void OnDragEnter(const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent) override; virtual void OnDragLeave(const FDragDropEvent& DragDropEvent) override; virtual void OnMouseEnter(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override; @@ -82,5 +83,7 @@ public: int32 OriginClipStartFrame = 0; int32 OriginClipEndFrame = 0; + AVFrame* LastFrame = nullptr; + }; diff --git a/Source/Cut5/Widgets/STimelineProperty.cpp b/Source/Cut5/Widgets/STimelineProperty.cpp index 72bf0bb..24b512c 100644 --- a/Source/Cut5/Widgets/STimelineProperty.cpp +++ b/Source/Cut5/Widgets/STimelineProperty.cpp @@ -33,6 +33,7 @@ void STimelineProperty::Construct(const FArguments& InArgs) [ SNew(SImage) .Image(FUtils::GetBrushFromImage(FUtils::GetResourcesPath(TimelinePropertyData.IconPath), {40, 40})) + .Visibility(TimelinePropertyData.Type == ETrackType::None ? EVisibility::Collapsed : EVisibility::Visible) ] + SOverlay::Slot() diff --git a/Source/Cut5/Widgets/STimelinePropertyPanel.cpp b/Source/Cut5/Widgets/STimelinePropertyPanel.cpp index 9b5024a..9ddcdeb 100644 --- a/Source/Cut5/Widgets/STimelinePropertyPanel.cpp +++ b/Source/Cut5/Widgets/STimelinePropertyPanel.cpp @@ -151,6 +151,22 @@ void STimelinePropertyPanel::Construct(const FArguments& InArgs) ]; AddNewSelection(FTimelinePropertyData(TEXT("聚光灯"), ETrackType::SpotLightTrack, TEXT("SpotLight.png"))); AddNewSelection(FTimelinePropertyData(TEXT("氛围灯"), ETrackType::AtomSphereLightTrack, TEXT("PlayerLight.png"))); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + AddNewSelection(FTimelinePropertyData(false)); + } void STimelinePropertyPanel::AddNewSelection(FTimelinePropertyData TimelinePropertyData) diff --git a/Source/Cut5/Widgets/STrackBody.cpp b/Source/Cut5/Widgets/STrackBody.cpp index 2e7beb2..03027b8 100644 --- a/Source/Cut5/Widgets/STrackBody.cpp +++ b/Source/Cut5/Widgets/STrackBody.cpp @@ -3,6 +3,7 @@ #include "STrackBody.h" +#include "SCutMainWindow.h" #include "SlateOptMacros.h" #include "Commands/TimelineClipCommands.h" #include "Cut5/Utils/FFMPEGUtils.h" @@ -26,6 +27,14 @@ void STrackBody::Construct(const FArguments& InArgs) { BreakClip(SelectedClipGUID); }), FCanExecuteAction()); + CommandList->MapAction(FTimelineClipCommands::Get().Fill2Start, FExecuteAction::CreateLambda([this]() + { + Fill2Start(SelectedClipGUID); + }), FCanExecuteAction()); + CommandList->MapAction(FTimelineClipCommands::Get().Fill2End, FExecuteAction::CreateLambda([this]() + { + Fill2End(SelectedClipGUID); + }), FCanExecuteAction()); MainWidgetInterface = InArgs._MainWidgetInterface; TrackHead = InArgs._TrackHead; @@ -66,15 +75,7 @@ void STrackBody::Construct(const FArguments& InArgs) void STrackBody::CallRender() { Overlay->ClearChildren(); - - for (TSharedPtr Clip : SlateClips) - { - if (Clip->SoundThread) - { - Clip->SoundThread->Stop(); - } - - } + SlateClips.Empty(); for (FClipData& TempClipData : TrackHead->TrackData.ClipData) @@ -166,6 +167,39 @@ void STrackBody::RemoveClip(const FGuid& Guid) CallRender(); } + +inline void STrackBody::Fill2Start(const FGuid& Guid) +{ + for (int32 i = 0; i < SlateClips.Num(); i++) + { + if (TrackHead->TrackData.ClipData[i].ClipGuid == Guid && + TrackHead->TrackData.ClipData[i].ClipType != ETrackType::VideoTrack || + TrackHead->TrackData.ClipData[i].ClipType != ETrackType::AudioTrack || + TrackHead->TrackData.ClipData[i].ClipType != ETrackType::AudioTrackR) + { + TrackHead->TrackData.ClipData[i].ClipStartFrame = 0; + CallRender(); + break; + } + } +} + +void STrackBody::Fill2End(const FGuid& Guid) +{ + for (int32 i = 0; i < SlateClips.Num(); i++) + { + if (TrackHead->TrackData.ClipData[i].ClipGuid == Guid && + TrackHead->TrackData.ClipData[i].ClipType != ETrackType::VideoTrack || + TrackHead->TrackData.ClipData[i].ClipType != ETrackType::AudioTrack || + TrackHead->TrackData.ClipData[i].ClipType != ETrackType::AudioTrackR) + { + TrackHead->TrackData.ClipData[i].ClipEndFrame = FGlobalData::TrackLength; + CallRender(); + break; + } + } +} + void STrackBody::BreakClip(const FGuid& Guid) { for (int32 i = 0; i < SlateClips.Num(); i++) diff --git a/Source/Cut5/Widgets/STrackBody.h b/Source/Cut5/Widgets/STrackBody.h index 484c9e5..48f05e8 100644 --- a/Source/Cut5/Widgets/STrackBody.h +++ b/Source/Cut5/Widgets/STrackBody.h @@ -34,6 +34,8 @@ public: virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; virtual void RemoveClip(const FGuid& Guid) override; virtual void BreakClip(const FGuid& Guid) override; + void Fill2Start(const FGuid& Guid); + void Fill2End(const FGuid& Guid); // virtual bool CanDragOver() override; @@ -55,9 +57,12 @@ public: }; + + + inline int32 STrackBody::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, - const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, - const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const + const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, + const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { if (bNeedRenderDragDropOver) diff --git a/Source/Cut5/Widgets/StatePanel/SLightArrayPanel.cpp b/Source/Cut5/Widgets/StatePanel/SLightArrayPanel.cpp index 8e968ee..551c419 100644 --- a/Source/Cut5/Widgets/StatePanel/SLightArrayPanel.cpp +++ b/Source/Cut5/Widgets/StatePanel/SLightArrayPanel.cpp @@ -67,7 +67,9 @@ int32 SLightArrayPanel::OnPaint(const FPaintArgs& Args, const FGeometry& Allotte // const FVector2D SingleLocalSize = // FVector2D(AllottedGeometry.AbsoluteToLocal(FVector2D(LightGrid->GetCachedGeometry().Size.X / FGlobalData::LightArrayX, LightGrid->GetCachedGeometry().Size.Y / FGlobalData::LightArrayY))); const FVector2D SingleLocalSize = - FVector2D(LightGrid->GetCachedGeometry().Size.X / FGlobalData::LightArrayX, LightGrid->GetCachedGeometry().Size.Y / FGlobalData::LightArrayY); + FVector2D((LightGrid->GetCachedGeometry().Size.X / FGlobalData::LightArrayX) / 2.0, (LightGrid->GetCachedGeometry().Size.Y / FGlobalData::LightArrayY) / 2.0); + const FVector2D OriginLocalSize = + FVector2D((LightGrid->GetCachedGeometry().Size.X / FGlobalData::LightArrayX), (LightGrid->GetCachedGeometry().Size.Y / FGlobalData::LightArrayY)); for (int32 i = 0; i < FGlobalData::LightArrayX; i++) { for (int32 j = 0; j < FGlobalData::LightArrayY; j++) @@ -76,7 +78,7 @@ int32 SLightArrayPanel::OnPaint(const FPaintArgs& Args, const FGeometry& Allotte FSlateDrawElement::MakeBox( OutDrawElements, LayerId, - LightGrid->GetPaintSpaceGeometry().ToPaintGeometry(SingleLocalSize, FSlateLayoutTransform(FVector2D(i * SingleLocalSize.X, j * SingleLocalSize.Y))), + LightGrid->GetPaintSpaceGeometry().ToPaintGeometry(SingleLocalSize, FSlateLayoutTransform(FVector2D(i * OriginLocalSize.X, j * OriginLocalSize.Y))), &Brush, ESlateDrawEffect::None, LightGridColors[j * FGlobalData::LightArrayX + i]